/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */
@layer properties {
  @supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))) {
    *,:before,:after,::backdrop {
      --tw-outline-style:solid
    }
  }
}

@layer theme {
  :root,:host {
    --font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
    --font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    --spacing:.25rem;
    --default-font-family:var(--font-sans);
    --default-mono-font-family:var(--font-mono)
  }
}

@layer base {
  *,:after,:before,::backdrop {
    box-sizing:border-box;
    border:0 solid;
    margin:0;
    padding:0
  }
  ::file-selector-button {
    box-sizing:border-box;
    border:0 solid;
    margin:0;
    padding:0
  }
  html,:host {
    -webkit-text-size-adjust:100%;
    tab-size:4;
    line-height:1.5;
    font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
    font-feature-settings:var(--default-font-feature-settings,normal);
    font-variation-settings:var(--default-font-variation-settings,normal);
    -webkit-tap-highlight-color:transparent
  }
  hr {
    height:0;
    color:inherit;
    border-top-width:1px
  }
  abbr:where([title]) {
    -webkit-text-decoration:underline dotted;
    text-decoration:underline dotted
  }
  h1,h2,h3,h4,h5,h6 {
    font-size:inherit;
    font-weight:inherit
  }
  a {
    color:inherit;
    -webkit-text-decoration:inherit;
    -webkit-text-decoration:inherit;
    -webkit-text-decoration:inherit;
    -webkit-text-decoration:inherit;
    text-decoration:inherit
  }
  b,strong {
    font-weight:bolder
  }
  code,kbd,samp,pre {
    font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
    font-feature-settings:var(--default-mono-font-feature-settings,normal);
    font-variation-settings:var(--default-mono-font-variation-settings,normal);
    font-size:1em
  }
  small {
    font-size:80%
  }
  sub,sup {
    vertical-align:baseline;
    font-size:75%;
    line-height:0;
    position:relative
  }
  sub {
    bottom:-.25em
  }
  sup {
    top:-.5em
  }
  table {
    text-indent:0;
    border-color:inherit;
    border-collapse:collapse
  }
  :-moz-focusring {
    outline:auto
  }
  progress {
    vertical-align:baseline
  }
  summary {
    display:list-item
  }
  ol,ul,menu {
    list-style:none
  }
  img,svg,video,canvas,audio,iframe,embed,object {
    vertical-align:middle;
    display:block
  }
  img,video {
    max-width:100%;
    height:auto
  }
  button,input,select,optgroup,textarea {
    font:inherit;
    font-feature-settings:inherit;
    font-variation-settings:inherit;
    letter-spacing:inherit;
    color:inherit;
    opacity:1;
    background-color:#0000;
    border-radius:0
  }
  ::file-selector-button {
    font:inherit;
    font-feature-settings:inherit;
    font-variation-settings:inherit;
    letter-spacing:inherit;
    color:inherit;
    opacity:1;
    background-color:#0000;
    border-radius:0
  }
  :where(select:is([multiple],[size])) optgroup {
    font-weight:bolder
  }
  :where(select:is([multiple],[size])) optgroup option {
    padding-inline-start:20px
  }
  ::file-selector-button {
    margin-inline-end:4px
  }
  ::placeholder {
    opacity:1
  }
  @supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px) {
    ::placeholder {
      color:currentColor
    }
    @supports (color:color-mix(in lab, red, red)) {
      ::placeholder {
        color:color-mix(in oklab, currentcolor 50%, transparent)
      }
    }
  }
  textarea {
    resize:vertical
  }
  ::-webkit-search-decoration {
    -webkit-appearance:none
  }
  ::-webkit-date-and-time-value {
    min-height:1lh;
    text-align:inherit
  }
  ::-webkit-datetime-edit {
    display:inline-flex
  }
  ::-webkit-datetime-edit-fields-wrapper {
    padding:0
  }
  ::-webkit-datetime-edit {
    padding-block:0
  }
  ::-webkit-datetime-edit-year-field {
    padding-block:0
  }
  ::-webkit-datetime-edit-month-field {
    padding-block:0
  }
  ::-webkit-datetime-edit-day-field {
    padding-block:0
  }
  ::-webkit-datetime-edit-hour-field {
    padding-block:0
  }
  ::-webkit-datetime-edit-minute-field {
    padding-block:0
  }
  ::-webkit-datetime-edit-second-field {
    padding-block:0
  }
  ::-webkit-datetime-edit-millisecond-field {
    padding-block:0
  }
  ::-webkit-datetime-edit-meridiem-field {
    padding-block:0
  }
  ::-webkit-calendar-picker-indicator {
    line-height:1
  }
  :-moz-ui-invalid {
    box-shadow:none
  }
  button,input:where([type=button],[type=reset],[type=submit]) {
    appearance:button
  }
  ::file-selector-button {
    appearance:button
  }
  ::-webkit-inner-spin-button {
    height:auto
  }
  ::-webkit-outer-spin-button {
    height:auto
  }
  [hidden]:where(:not([hidden=until-found])) {
    display:none!important
  }
}

@layer components;
@layer utilities {
  .relative {
    position:relative
  }
  .start {
    inset-inline-start:var(--spacing)
  }
  .grid {
    display:grid
  }
  .table {
    display:table
  }
  .outline {
    outline-style:var(--tw-outline-style);
    outline-width:1px
  }
}

:root {
  --ink:#101112;
  --paper:#f7f7f3;
  --white:#fff;
  --mist:#e9ece8;
  --cyan:#13cbd7;
  --coral:#ff6b56;
  --yellow:#ffd447;
  --violet:#7b5cff;
  --line:#10111226;
  --radius:26px;
  --shadow:0 24px 80px #1214141f
}

* {
  box-sizing:border-box
}

html {
  scroll-behavior:smooth;
  /* Keeps anchored sections clear of the fixed header. */
  scroll-padding-top:80px
}

body {
  background:var(--paper);
  color:var(--ink);
  -webkit-font-smoothing:antialiased;
  margin:0;
  font-family:Arial,Helvetica,sans-serif
}

body,
button,
input,
textarea,
select {
  font:inherit
}

a {
  color:inherit;
  text-decoration:none
}

button,
a {
  -webkit-tap-highlight-color:transparent
}

img {
  width:100%;
  display:block
}

::selection {
  background:var(--yellow);
  color:var(--ink)
}

[data-reveal] {
  opacity:0;
  transition:opacity .75s,transform .75s cubic-bezier(.2,.8,.2,1);
  transition-delay:var(--delay,0s);
  transform:translateY(30px)
}

[data-reveal].is-visible {
  opacity:1;
  transform:none
}

[data-parallax] {
  transform:translate3d(0, var(--parallax,0), 0) scale(1.045);
  transition:transform .1s linear
}

.site-header {
  z-index:20;
  justify-content:space-between;
  align-items:center;
  height:84px;
  padding:0 clamp(22px,4vw,72px);
  display:flex;
  position:fixed;
  inset:0 0 auto;
  transition:height .3s ease, transform .35s ease
}

/* The frosted bar lives on a pseudo-element, NOT on .site-header itself.
   backdrop-filter (like transform) turns an element into the containing
   block for its position:fixed children — which would break the full-screen
   mobile nav overlay nested inside this header. */
.site-header:before {
  content:"";
  z-index:-1;
  background:#ffffffb8;
  border-bottom:1px solid #1011121a;
  opacity:0;
  transition:opacity .3s ease;
  -webkit-backdrop-filter:blur(14px) saturate(160%);
  backdrop-filter:blur(14px) saturate(160%);
  position:absolute;
  inset:0
}

/* Past the hero: shorter bar, frosted background. */
.site-header.is-stuck {
  height:64px
}

.site-header.is-stuck:before {
  opacity:1
}

.site-header.is-stuck .logo-type strong {
  font-size:23px
}

.site-header.is-stuck .nav-cta {
  padding:10px 16px
}

/* Out of the way going down, back on the way up. */
.site-header.is-hidden {
  transform:translateY(-100%)
}

/* Belt and braces with the JS guard: a transform here would re-anchor the
   open mobile menu, so it must never apply while that menu is showing. */
.site-header:has(nav.nav-open) {
  transform:none
}

.logo {
  z-index:22;
  align-items:center;
  line-height:1;
  display:inline-flex;
  position:relative
}

.logo-type {
  gap:3px;
  display:grid
}

.logo-type strong {
  letter-spacing:-.035em;
  align-items:flex-start;
  font-size:28px;
  font-weight:400;
  display:flex
}

.logo-type strong b {
  font-weight:950
}

.logo-type strong span {
  font-weight:400
}

/* The orange spark dot that sat top-right of the wordmark was removed on
   2026-08-16, from the header, the footer and 404.html. Its <i class="logo-
   spark"> element is gone from the markup too, so this rule went with it —
   it was self-contained (an 8px dot with its own 6px left margin) and
   nothing else depended on it. */

.logo-type small {
  letter-spacing:.36em;
  padding-left:1px;
  font-size:7px;
  font-weight:850
}

.logo-light {
  color:var(--white)
}

.site-header nav {
  align-items:center;
  gap:clamp(18px,2.4vw,40px);
  font-size:13px;
  font-weight:700;
  display:flex
}

.site-header nav>a:not(.nav-cta) {
  position:relative
}

.site-header nav>a:not(.nav-cta):after {
  content:"";
  background:var(--ink);
  height:2px;
  transition:right .25s;
  position:absolute;
  inset:auto 100% -7px 0
}

.site-header nav>a:hover:after {
  right:0
}

.nav-cta {
  background:var(--ink);
  color:#fff;
  border-radius:999px;
  align-items:center;
  gap:14px;
  padding:13px 18px;
  display:inline-flex
}

.arrow {
  transition:transform .25s;
  display:inline-flex
}

a:hover .arrow,
button:hover .arrow {
  transform:translate(3px,-3px)
}

.arrow-down {
  transform:rotate(90deg)
}

a:hover .arrow-down {
  transform:rotate(90deg)translate(3px,-3px)
}

.menu-toggle {
  z-index:22;
  background:0 0;
  border:0;
  width:42px;
  height:42px;
  padding:10px;
  display:none;
  position:relative
}

.menu-toggle span {
  background:var(--ink);
  height:2px;
  margin:7px 0;
  display:block
}

.hero {
  background:#fff;
  align-items:center;
  height:min(900px,100vh);
  min-height:820px;
  padding:120px clamp(22px,6vw,112px) 90px;
  display:flex;
  position:relative;
  overflow:hidden
}

.hero:after {
  content:"";
  background:linear-gradient(180deg, #f7f7f300 0%, #f7f7f31f 20%, #f7f7f3ad 62%, var(--paper) 100%);
  filter:blur(1px);
  z-index:4;
  pointer-events:none;
  height:170px;
  position:absolute;
  bottom:-1px;
  left:-4%;
  right:-4%
}

.hero-image {
  position:absolute;
  inset:0 0 -48px 34%;
  overflow:hidden;
  -webkit-mask-image:linear-gradient(#000 0% 80%,#000000e6 86%,#00000047 96%,#0000 100%);
  mask-image:linear-gradient(#000 0% 80%,#000000e6 86%,#00000047 96%,#0000 100%);
  opacity:1
}

.hero-image:after {
  content:"";
  background:linear-gradient(90deg,#fff 0%,#ffffffeb 10%,#ffffff1a 41%,#0000 70%),linear-gradient(#ffffff1f,#0000 45%);
  position:absolute;
  inset:0
}

.hero-image img {
  object-fit:cover;
  object-position:61% center;
  height:108%
}

.hero-copy {
  z-index:5;
  max-width:690px;
  position:relative
}

.eyebrow,
.section-label {
  letter-spacing:.19em;
  text-transform:uppercase;
  align-items:center;
  gap:10px;
  font-size:11px;
  font-weight:900;
  display:flex
}

.eyebrow span {
  background:var(--coral);
  width:8px;
  height:8px;
  box-shadow:13px 0 0 var(--cyan), 26px 0 0 var(--yellow);
  border-radius:50%;
  margin-right:26px
}
/* HERO HEADLINE TYPEFACE — changed 2026-08-26.
   The hero h1 was the only heading on the site set in Arial Black 900 with
   -.085em tracking and .82 leading; every other heading (.promise h2,
   .section-heading h2, .admin-intro h2, .research h2, .work-heading h2,
   .free-copy h2, .contact h2 — see the shared rule above) is Arial 700 at
   -.055em / .98. Face, weight and tracking are matched, so the hero reads as
   part of the same family. LEADING IS THE ONE DELIBERATE EXCEPTION: matching
   .98 opened too big a gap between "Your idea" and "Made visible" at hero
   size, so it was tightened to .86 on 2026-08-26 at the user's request.
   Compared .98 / .88 / .86 / .82 on the live page: .82 (the original Arial
   Black value) brings the "y" descender of "Your" almost onto the caps of
   "Made", .86 is the tightest that still clears it.
   The clamp()ed size is deliberately still bigger than an h2, and .hero h1 em
   inherits all of this via its `font:inherit`.
   TO REVERSE: letter-spacing -.085em, font-family "Arial Black",Arial,
   Helvetica,sans-serif, font-weight 900, line-height .82. */

.hero h1 {
  letter-spacing:-.055em;
  max-width:750px;
  margin:30px 0 24px;
  font-family:Arial,Helvetica,sans-serif;
  font-size:clamp(64px,7.6vw,122px);
  font-weight:700;
  font-synthesis:none;
  line-height:.86
}

.hero h1 em {
  color:rgba(255,107,86,.58);
  -webkit-text-fill-color:transparent;
  /* 2026-08-26: outline dropped at the user's request — "Made visible" is now
     pure gradient fill, no stroke. Kept as an explicit `0` rather than a
     deleted line because the later `.hero h1 em` rule (search
     "-webkit-text-stroke-color") still exists; width 0 makes any colour there
     a no-op. TO REVERSE: `-webkit-text-stroke:1px var(--ink);` here, and
     un-comment the stroke-colour line in that later rule. */
  -webkit-text-stroke:0;
  background:linear-gradient(90deg,rgba(137,48,190,.58) 0%,rgba(231,58,145,.58) 18%,rgba(255,78,35,.58) 34%,rgba(255,137,0,.58) 45%,rgba(255,194,24,.58) 55%,rgba(0,190,204,.58) 68%,rgba(0,129,225,.58) 82%,rgba(0,193,218,.58) 100%);
  background-size:100% 100%;
  background-position:left center;
  background-repeat:no-repeat;
  padding-right:.09em;
  margin-right:-.09em;
  -webkit-background-clip:text;
  background-clip:text;
  letter-spacing:inherit;
  font:inherit;
  /* 2026-08-26: "Made visible" set to regular, matching the 400-weight
     `h2 span` subheadings elsewhere on the site. MUST stay below the
     `font:inherit` above — that shorthand resets font-weight back to the
     h1's 700, so an earlier declaration would be wiped out.
     TO REVERSE: delete this line. */
  font-weight:400;
  font-size:calc(1em - 10px);
  font-kerning:inherit;
  display:inline-block
}

.hero-intro {
  max-width:525px;
  margin:0;
  font-size:clamp(18px,1.45vw,23px);
  line-height:1.48
}

.hero-actions {
  align-items:center;
  gap:28px;
  margin-top:38px;
  display:flex
}

.button {
  cursor:pointer;
  border:0;
  border-radius:999px;
  justify-content:space-between;
  align-items:center;
  gap:26px;
  padding:16px 22px;
  font-size:13px;
  font-weight:850;
  transition:transform .2s,box-shadow .2s,background .2s;
  display:inline-flex
}

.button:hover {
  transform:translateY(-2px);
  box-shadow:0 10px 28px #00000029
}

.button-dark {
  background:var(--ink);
  color:#fff
}

.button-light {
  color:var(--ink);
  background:#fff
}

.text-link {
  border-bottom:1px solid var(--ink);
  align-items:center;
  gap:12px;
  padding:8px 0;
  font-size:13px;
  font-weight:850;
  display:inline-flex
}

.hero-proof {
  z-index:5;
  color:#fff;
  background:var(--ink);
  border-radius:50%;
  flex-direction:column;
  justify-content:center;
  align-items:center;
  width:166px;
  height:166px;
  display:flex;
  position:absolute;
  bottom:102px;
  right:clamp(20px,3vw,54px);
  transform:rotate(6deg);
  box-shadow:0 0 0 9px #ffffffd9
}

.hero-proof strong {
  letter-spacing:-.06em;
  font-size:43px;
  line-height:.9
}

.hero-proof span {
  text-align:center;
  text-transform:uppercase;
  letter-spacing:.12em;
  margin-top:8px;
  font-size:10px;
  font-weight:800;
  line-height:1.25
}

.scroll-cue {
  z-index:5;
  letter-spacing:.18em;
  text-transform:uppercase;
  align-items:center;
  gap:12px;
  font-size:9px;
  font-weight:800;
  display:flex;
  position:absolute;
  bottom:78px;
  left:clamp(22px,4vw,72px)
}

.scroll-cue i {
  background:var(--ink);
  width:46px;
  height:1px;
  position:relative
}

.scroll-cue i:after {
  content:"";
  border-bottom:1px solid;
  border-right:1px solid;
  width:7px;
  height:7px;
  position:absolute;
  top:-3px;
  right:0;
  transform:rotate(45deg)
}

.promise {
  padding:18px 0 110px;
  overflow:hidden
}

.section-grid {
  grid-template-columns:minmax(140px,.38fr) 1.62fr;
  gap:50px;
  max-width:1380px;
  margin:auto;
  padding:0 clamp(22px,5vw,84px);
  display:grid
}

.section-label {
  align-items:flex-start;
  margin:8px 0 0
}

.section-label:before {
  content:"";
  background:currentColor;
  width:26px;
  height:2px;
  margin-top:5px
}

.promise h2,
.section-heading h2,
.admin-intro h2,
.research h2,
.work-heading h2,
.free-copy h2,
.contact h2 {
  letter-spacing:-.055em;
  margin:0;
  font-size:clamp(42px,5.2vw,80px);
  line-height:.98;
  font-weight:700
}

h2 span:not(.promise-word, .promise-char, .work-word, .work-char) {
  color:#8a8d8b;
  font-weight:400
}

.promise-points {
  grid-template-columns:repeat(3,1fr);
  gap:38px;
  margin-top:66px;
  display:grid
}

.promise-points article {
  border-top:1px solid var(--ink);
  padding-top:15px
}

.promise-points b {
  color:#8a8d8b;
  font-size:10px
}

.promise-points h3 {
  margin:38px 0 12px;
  font-size:19px
}

.promise-points p {
  color:#616563;
  margin:0;
  font-size:14px;
  line-height:1.65
}

.section-pad {
  padding:120px clamp(22px,5vw,84px)
}

.pricing {
  background:#fff;
  position:relative
}

.pricing:before {
  content:"";
  pointer-events:none;
  background:radial-gradient(circle at 8% 19%,#13cbd714,#0000 18%),radial-gradient(circle at 92% 8%,#ff6b5614,#0000 17%);
  position:absolute;
  inset:0
}

.section-heading {
  grid-template-columns:.5fr 1.25fr .75fr;
  align-items:end;
  gap:44px;
  max-width:1380px;
  margin:auto;
  display:grid;
  position:relative
}

.section-heading>p:last-child {
  color:#5c605e;
  margin:0 0 7px;
  font-size:16px;
  line-height:1.6
}

.price-grid {
  grid-template-columns:repeat(3,1fr);
  gap:18px;
  max-width:1380px;
  margin:72px auto 0;
  display:grid;
  position:relative
}

.price-card {
  border-radius:var(--radius);
  background:var(--paper);
  border:1px solid #d9dcda;
  transition:transform .35s,box-shadow .35s;
  overflow:hidden
}

.price-card:hover {
  box-shadow:var(--shadow);
  transform:translateY(-8px)
}

.price-image {
  background:#dfe3e0;
  height:210px;
  position:relative;
  overflow:hidden
}

.price-image:after {
  content:"";
  background:linear-gradient(#0000 55%,#00000029);
  position:absolute;
  inset:0
}

.price-image img {
  object-fit:cover;
  object-position:top;
  width:100%;
  height:100%;
  transition:transform .7s
}

.price-card:hover .price-image img {
  transform:scale(1.04)
}

.price-body {
  padding:27px 28px 30px
}

.price-top {
  justify-content:space-between;
  gap:20px;
  display:flex
}

.price-top p {
  text-transform:uppercase;
  letter-spacing:.18em;
  margin:0 0 8px;
  font-size:10px;
  font-weight:900
}

.price-top h3 {
  letter-spacing:-.02em;
  margin:0;
  font-size:21px
}

.price {
  letter-spacing:-.06em;
  align-items:flex-start;
  gap:9px;
  margin:34px 0 10px;
  font-size:56px;
  font-weight:950;
  line-height:1;
  display:flex
}

.price small {
  text-transform:uppercase;
  letter-spacing:.13em;
  margin-top:7px;
  font-size:9px;
  font-weight:800
}

.price-note {
  color:#666a68;
  min-height:42px;
  font-size:14px;
  line-height:1.55
}

.price-card ul {
  border-top:1px solid #d3d6d4;
  margin:24px 0 0;
  padding:23px 0;
  list-style:none
}

.price-card li {
  gap:10px;
  margin:12px 0;
  font-size:13px;
  line-height:1.4;
  display:flex
}

.price-card li span {
  color:#0999a2;
  font-weight:900
}

.hosting {
  color:#5a5e5c;
  background:#fff;
  border-radius:11px;
  gap:4px;
  min-height:65px;
  padding:15px;
  font-size:11px;
  line-height:1.45;
  display:grid
}

.hosting span {
  color:var(--ink);
  text-transform:uppercase;
  letter-spacing:.15em;
  font-size:8px;
  font-weight:900
}

.button-card {
  background:var(--ink);
  color:#fff;
  width:100%;
  margin-top:20px
}

.price-card.elevate {
  background:#eaf9f8;
  border-color:#b8e2e1
}

.price-card.elevate .button-card {
  background:#0e666b
}

.price-card.signature {
  background:var(--ink);
  color:#fff;
  border-color:var(--ink)
}

.price-card.signature .price-note,
.price-card.signature li {
  color:#c9ccca
}

.price-card.signature ul {
  border-color:#393c3b
}

.price-card.signature .hosting {
  color:#dadcdb;
  background:#202322
}

.price-card.signature .hosting span {
  color:#fff
}

.price-card.signature .button-card {
  background:var(--yellow);
  color:var(--ink)
}

.pricing-footnote {
  text-align:center;
  color:#656967;
  max-width:620px;
  margin:42px auto 0;
  font-size:13px;
  line-height:1.6
}

.admin-demo {
  background:var(--ink);
  color:#fff;
  grid-template-columns:.72fr 1.28fr;
  align-items:center;
  gap:clamp(40px,7vw,100px);
  display:grid
}

.light-label {
  color:#afb2b0
}

.admin-intro h2 {
  margin:24px 0
}

.admin-intro h2 span,
.contact h2 span {
  color:#9aa0a0
}

.admin-intro>p:last-child {
  color:#bcc0bd;
  max-width:470px;
  font-size:16px;
  line-height:1.7
}

.admin-graphic {
  background:#151716;
  border:1px solid #373a38;
  border-radius:18px;
  width:100%;
  overflow:hidden;
  box-shadow:0 35px 120px #0000006b
}

.admin-graphic img {
  width:100%;
  height:auto;
  display:block
}

.research {
  background:#fff;
  align-items:center;
  min-height:790px;
  display:flex;
  position:relative;
  overflow:hidden
}

.research:after {
  content:"";
  z-index:1;
  pointer-events:none;
  background:linear-gradient(90deg, transparent 0%, transparent 45%, #ffffff94 61%, #fffffff0 100%), linear-gradient(180deg, var(--paper) 0%, transparent 9%, transparent 88%, var(--paper) 100%);
  position:absolute;
  inset:0
}

.research-image {
  position:absolute;
  inset:-3% 0;
  overflow:hidden;
  -webkit-mask-image:linear-gradient(#0000 0%,#000 8% 90%,#0000 100%);
  mask-image:linear-gradient(#0000 0%,#000 8% 90%,#0000 100%)
}

.research-image img {
  object-fit:cover;
  object-position:left center;
  width:100%;
  height:106%
}

.research-copy {
  z-index:2;
  width:min(48%,650px);
  margin-left:auto;
  padding:110px clamp(22px,5vw,84px) 110px 20px;
  position:relative
}

.research-copy h2 {
  margin:24px 0 30px
}

.research-copy .lead {
  color:#5f6361;
  font-size:16px;
  line-height:1.7
}

.process-list {
  border-top:1px solid var(--ink);
  margin:38px 0 0;
  padding:0;
  list-style:none
}

.process-list li {
  color:#6e7270;
  border-bottom:1px solid #cfd2d0;
  grid-template-columns:48px 1fr;
  padding:16px 0;
  font-size:12px;
  display:grid
}

.process-list b {
  color:var(--coral);
  font-size:9px
}

.process-list span {
  grid-template-columns:1fr 1.5fr;
  gap:15px;
  display:grid
}

.process-list strong {
  color:var(--ink)
}

.work {
  background:#e9eae5
}

.work-heading {
  justify-content:space-between;
  align-items:end;
  gap:60px;
  max-width:1380px;
  margin:auto 68px;
  display:flex
}

.work-heading h2 {
  margin-top:23px
}

.work-heading>p {
  color:#5d615f;
  max-width:440px;
  font-size:15px;
  line-height:1.65
}

.work-grid {
  grid-template-columns:repeat(12,1fr);
  gap:20px;
  max-width:1380px;
  margin:auto;
  display:grid
}

.work-card {
  background:#fff;
  border-radius:20px;
  grid-column:span 6;
  transition:transform .35s,box-shadow .35s;
  overflow:hidden;
  box-shadow:0 8px 30px #1e232014
}

.work-card:hover {
  box-shadow:var(--shadow);
  transform:translateY(-7px)
}

.work-3 {
  grid-column:span 7
}

.work-browser {
  background:#f9faf8;
  border-bottom:1px solid #e0e2df;
  grid-template-columns:1fr 2fr 1fr;
  align-items:center;
  height:35px;
  padding:0 13px;
  display:grid
}

.work-browser>span {
  gap:4px;
  display:flex
}

.work-browser i {
  background:#c4c8c5;
  border-radius:50%;
  width:5px;
  height:5px
}

.work-browser small {
  text-align:center;
  color:#888c89;
  white-space:nowrap;
  background:#eceeeb;
  border-radius:999px;
  padding:4px 10px;
  font-size:7px;
  overflow:hidden
}

.work-shot {
  background:#dfe2df;
  height:400px;
  overflow:hidden
}

.work-shot img {
  object-fit:cover;
  object-position:top;
  width:100%;
  height:100%;
  transition:transform 1.2s cubic-bezier(.2,.8,.2,1)
}

.work-card:hover .work-shot img {
  transform:scale(1.035)
}

.work-meta {
  justify-content:space-between;
  align-items:center;
  padding:20px 22px;
  display:flex
}

.work-meta>span {
  gap:4px;
  display:grid
}

.work-meta b {
  font-size:16px
}

.work-meta small {
  color:#7b7f7c;
  font-size:10px
}

.work-placeholder {
  border-radius:20px;
  flex-direction:column;
  grid-column:span 5;
  justify-content:space-between;
  min-height:300px;
  padding:34px;
  display:flex;
  position:relative;
  overflow:hidden
}

.work-placeholder>span {
  letter-spacing:.18em;
  text-transform:uppercase;
  font-size:9px;
  font-weight:900
}

.work-placeholder strong {
  letter-spacing:-.05em;
  font-size:clamp(30px,3.3vw,50px);
  line-height:.95
}

.work-placeholder a {
  align-items:center;
  gap:10px;
  font-size:12px;
  font-weight:900;
  display:flex
}

.placeholder-one {
  color:#fff;
  background:var(--violet)
}

.placeholder-one:after {
  content:"";
  border:38px solid var(--yellow);
  opacity:.9;
  border-radius:50%;
  width:260px;
  height:260px;
  position:absolute;
  top:-70px;
  right:-65px
}

.placeholder-two {
  background:var(--cyan);
  min-height:260px;
  color:var(--ink);
  grid-column:span 6
}

.placeholder-two i {
  background:var(--coral);
  border-radius:47% 53% 64% 36%/38% 44% 56% 62%;
  width:180px;
  height:180px;
  position:absolute;
  bottom:-35px;
  right:-20px;
  transform:rotate(19deg)
}

.free-offer {
  background:#fff;
  grid-template-columns:1fr .85fr;
  align-items:center;
  gap:clamp(40px,7vw,110px);
  min-height:auto;
  padding:100px clamp(22px,6vw,110px);
  display:grid;
  position:relative;
  overflow:hidden
}

.free-image {
  position:absolute;
  inset:-60px 0;
  overflow:hidden;
  -webkit-mask-image:linear-gradient(#0000 0%,#000 12% 86%,#0000 100%);
  mask-image:linear-gradient(#0000 0%,#000 12% 86%,#0000 100%)
}

.free-image img {
  object-fit:cover;
  object-position:center;
  width:100%;
  height:108%
}

.free-image:after {
  content:"";
  background:linear-gradient(90deg,#fffffff7 0,#ffffffe6 37%,#ffffff2e 68%,#ffffffbd 100%);
  position:absolute;
  inset:0
}

.free-copy,
.idea-form {
  z-index:2;
  position:relative
}

.free-copy {
  max-width:620px
}

.free-copy h2 {
  margin:26px 0
}

.free-copy h2 span {
  color:#8a8d8b
}

.free-copy>p:not(.section-label) {
  max-width:560px;
  font-size:17px;
  line-height:1.65
}

.free-copy .small-print {
  color:#656967;
  border-left:2px solid var(--coral);
  padding-left:14px;
  font-size:11px!important
}

.idea-form {
  -webkit-backdrop-filter:blur(10px);
  backdrop-filter:blur(10px);
  box-shadow:var(--shadow);
  background:#ffffffed;
  border:1px solid #d9dcda;
  border-radius:20px;
  padding:28px
}

.field-row {
  grid-template-columns:1fr 1fr;
  gap:14px;
  display:grid
}

.idea-form label,
.contact-form label {
  letter-spacing:.15em;
  text-transform:uppercase;
  gap:8px;
  font-size:9px;
  font-weight:900;
  display:grid
}

.idea-form input,
.idea-form textarea,
.contact-form input,
.contact-form textarea,
.contact-form select {
  width:100%;
  color:var(--ink);
  letter-spacing:0;
  text-transform:none;
  resize:vertical;
  background:#fff;
  border:1px solid #cbd0cc;
  border-radius:8px;
  outline:none;
  padding:13px;
  font-size:13px
}

.idea-form input:focus,
.idea-form textarea:focus,
.contact-form input:focus,
.contact-form textarea:focus,
.contact-form select:focus {
  border-color:var(--ink);
  box-shadow:0 0 0 3px #13cbd72b
}

.idea-form>label {
  margin-top:15px
}

.idea-form .button {
  width:100%;
  margin-top:18px
}

.form-success {
  color:#175d32;
  background:#def7e6;
  border-radius:8px;
  margin:14px 0 0;
  padding:11px 13px;
  font-size:11px;
  line-height:1.45
}

.contact {
  background:var(--ink);
  color:#fff;
  grid-template-columns:1fr 1fr;
  align-items:start;
  gap:clamp(60px,9vw,150px);
  display:grid
}

.contact h2 {
  margin:28px 0
}

.contact-intro>p:not(.section-label) {
  color:#b9bdba;
  max-width:520px;
  font-size:16px;
  line-height:1.7
}

.contact-facts {
  flex-wrap:wrap;
  gap:8px;
  margin-top:34px;
  display:flex
}

.contact-facts span {
  color:#bfc2c0;
  text-transform:uppercase;
  letter-spacing:.1em;
  border:1px solid #404341;
  border-radius:999px;
  padding:9px 13px;
  font-size:9px
}

.contact-form {
  gap:16px;
  max-width:620px;
  display:grid
}

.contact-form label {
  color:#bfc2c0
}

.contact-form input,
.contact-form textarea,
.contact-form select {
  color:#fff;
  background:#202322;
  border-color:#3e4240
}

.contact-form select {
  appearance:none;
  background-image:linear-gradient(45deg,#0000 50%,#aaa 50%),linear-gradient(135deg,#aaa 50%,#0000 50%);
  background-position:calc(100% - 17px) 18px,calc(100% - 12px) 18px;
  background-repeat:no-repeat;
  background-size:5px 5px
}

.contact-form .button {
  margin-top:5px
}

.dark-success {
  color:#9ce4b5;
  background:#173821
}

footer {
  background:var(--ink);
  color:#fff;
  border-top:1px solid #343735;
  grid-template-columns:1fr 1fr auto;
  align-items:end;
  gap:30px;
  padding:44px clamp(22px,5vw,84px);
  display:grid
}

footer p {
  color:#9fa3a0;
  margin:0;
  font-size:12px
}

footer>a {
  text-transform:uppercase;
  letter-spacing:.12em;
  font-size:11px;
  font-weight:900
}

footer small {
  color:#6d716e;
  grid-column:2;
  font-size:9px
}

@media (width<=1080px) {
  .site-header nav {
    gap:18px
  }
  .site-header nav>a:not(.nav-cta) {
    display:none
  }
  .hero {
    min-height:760px
  }
  .hero-image {
    left:28%
  }
  .hero-copy {
    max-width:600px
  }
  .price-grid {
    grid-template-columns:1fr;
    gap:24px;
    max-width:700px
  }
  .price-card {
    grid-template-columns:42% 58%;
    display:grid
  }
  .price-image {
    height:auto;
    min-height:100%
  }
  .price-image img {
    height:100%
  }
  .price-note,.hosting {
    min-height:0
  }
  .admin-demo {
    grid-template-columns:1fr
  }
  .admin-intro {
    max-width:720px
  }
  .research {
    min-height:740px
  }
  .research-copy {
    width:min(50%,590px);
    padding-left:0
  }
  .work-shot {
    height:330px
  }
  .free-offer {
    grid-template-columns:1fr 1fr
  }
}

@media (width<=760px) {
  .site-header {
    height:74px;
    padding:0 20px
  }
  .site-header.is-stuck {
    height:58px
  }
  .logo-type strong {
    font-size:24px
  }
  .site-header.is-stuck .logo-type strong {
    font-size:21px
  }
  .menu-toggle {
    display:block
  }
  .site-header nav {
    background:var(--paper);
    opacity:0;
    visibility:hidden;
    flex-direction:column;
    justify-content:flex-start;
    align-items:stretch;
    gap:0;
    padding:110px 24px 40px;
    transition:all .25s;
    position:fixed;
    inset:0;
    transform:translateY(-15px)
  }
  .site-header nav.nav-open {
    opacity:1;
    visibility:visible;
    transform:none
  }
  .site-header nav>a:not(.nav-cta) {
    border-top:1px solid var(--line);
    padding:18px 0;
    font-size:22px;
    display:block
  }
  .site-header nav>a:not(.nav-cta):after {
    display:none
  }
  .nav-cta {
    justify-content:space-between;
    margin-top:20px;
    padding:17px 20px
  }
  .hero {
    align-items:flex-start;
    height:auto;
    min-height:780px;
    padding:110px 20px 115px
  }
  .hero-image {
    height:520px;
    inset:265px 0 0
  }
  .hero-image:after {
    background:linear-gradient(#fff 0,#ffffffd4 15%,#0000 45%),linear-gradient(90deg,#ffffff6b,#0000)
  }
  .hero-image img {
    object-position:61% center
  }
  .hero-copy {
    max-width:none
  }
  .eyebrow {
    font-size:9px
  }
  .hero h1 {
    margin:24px 0 20px;
    font-size:clamp(58px,20vw,78px)
  }
  .hero-intro {
    max-width:340px;
    font-size:16px
  }
  .hero-actions {
    flex-direction:column;
    align-items:flex-start;
    gap:12px;
    margin-top:28px
  }
  .hero-proof {
    width:116px;
    height:116px;
    bottom:74px;
    right:20px
  }
  .hero-proof strong {
    font-size:30px
  }
  .hero-proof span {
    font-size:7px
  }
  .scroll-cue {
    display:none
  }
  .hero:after {
    height:120px
  }
  .promise {
    padding-bottom:80px
  }
  .section-grid,.section-heading {
    grid-template-columns:1fr;
    gap:24px
  }
  .promise-points {
    grid-template-columns:1fr;
    gap:30px;
    margin-top:45px
  }
  .promise-points h3 {
    margin-top:20px
  }
  .section-pad {
    padding:82px 20px
  }
  .section-heading>p:last-child {
    max-width:500px
  }
  .price-grid {
    margin-top:48px
  }
  .price-card {
    visibility:visible;
    display:block
  }
  .price-card[data-reveal] {
    opacity:1;
    transition:transform .35s,box-shadow .35s;
    transform:none
  }
  .price-image {
    height:200px;
    min-height:0
  }
  .price-body {
    padding:24px 22px
  }
  .price {
    font-size:50px
  }
  .admin-demo {
    gap:40px
  }
  .research {
    min-height:0;
    padding-top:420px;
    display:block
  }
  .research:after {
    background:linear-gradient(180deg, #ffffff0a 0%, transparent 61%, var(--paper) 100%);
    height:440px;
    inset:0 0 auto
  }
  .research-image {
    height:440px;
    inset:0 0 auto;
    -webkit-mask-image:linear-gradient(#000 0% 77%,#0000 100%);
    mask-image:linear-gradient(#000 0% 77%,#0000 100%)
  }
  .research-image img {
    object-position:29% center;
    height:106%
  }
  .research-copy {
    width:100%;
    max-width:none;
    margin:0;
    padding:42px 20px 82px
  }
  .process-list span {
    grid-template-columns:1fr;
    gap:4px
  }
  .work-heading {
    margin-bottom:44px;
    display:block
  }
  .work-heading>p {
    margin-top:25px
  }
  .work-grid {
    grid-template-columns:1fr;
    display:grid
  }
  .work-card,.work-3,.work-placeholder,.placeholder-two {
    grid-column:1
  }
  .work-shot {
    height:300px
  }
  .work-placeholder {
    min-height:260px
  }
  .free-offer {
    grid-template-columns:1fr;
    align-content:center;
    gap:32px;
    min-height:auto;
    padding:80px 20px 90px
  }
  .free-copy {
    margin-top:0
  }
  .free-copy>p:not(.section-label) {
    font-size:15px
  }
  .idea-form {
    padding:20px
  }
  .field-row {
    grid-template-columns:1fr
  }
  .contact {
    grid-template-columns:1fr;
    gap:48px
  }
  footer {
    grid-template-columns:1fr auto;
    align-items:center
  }
  footer p,footer small {
    grid-column:1/-1
  }
}

/* ============================================================================
   BEFORE DESIGN — HEADING ONTO THE IMAGE, MOBILE ONLY  —  added 2026-09-04
   ----------------------------------------------------------------------------
   On a phone, .research stacks the umbrella photo (440px, pinned to the top
   via inset:0 0 auto - see the width<=760px block above) above .research-copy,
   which opens with "Before design" / "We learn your world before we shape
   it." At the user's request, that label+heading pair now sits on the photo
   itself, in the pale sky at the top of the frame, and .research-copy's next
   child - the "Good design starts with good questions..." paragraph - moves
   up to where the label used to be, right under the image.

   .research-heading is a plain wrapping <div> around the label and h2 in
   index.php (search "research-heading"). On every width above this one it is
   an unstyled block with no rules of its own, so margins collapse straight
   through it exactly as if it were not there - the desktop side-by-side
   layout (image left, copy right, both inside .research's flex row) is
   untouched, and nothing here needed a display:contents escape hatch for
   that.

   WHY .research-copy LOSES position:relative HERE: .research-heading needs
   position:absolute to come out of flow (so the paragraph and list behind it
   move up to fill the gap) and to be placed against the photo rather than
   against .research-copy's own box - .research-copy starts BELOW the image
   in the mobile flow (padding-top:420px on .research already clears it), so
   positioning against .research-copy would land the heading down at the
   paragraph, not up on the sky. .research itself is already position:relative
   (it is what .research-image is absolutely positioned against, unchanged
   above), so dropping .research-copy's own position:relative lets the
   heading's containing block resolve to .research instead - one level up,
   onto the same box the photo uses. .research-copy did not need position:
   relative for anything else at this width: its z-index:2 (for the desktop
   photo overlay) does nothing without a position value, and nothing else
   inside it is absolutely positioned.

   top:38px lands the block in the upper third of the 440px image, clear of
   the umbrella and the model's head start further down the frame - checked
   against the actual photo, not assumed. Text stays var(--ink) black, the
   same colour it already was: the sky here is a pale, even grey-white with
   no dark passages behind where the text sits, so no scrim or colour change
   was needed to keep it readable.

   FIXED 2026-09-04: reported as "the heading jumps from where it was to
   where it is" on a fresh page load. The cause is a second containing-block
   rule this comment did not originally account for - transform, not just
   position, creates one. .research-copy carries data-reveal, whose entrance
   animation is `transform: translateY(30px)` easing to `transform: none`
   over .75s (see [data-reveal] near the top of this file). ANY element with
   a transform other than none is a containing block for its absolutely
   positioned descendants, REGARDLESS of position:static - so for the ~.75s
   the slide-in is actually running, .research-heading was positioning
   against .research-copy's box again (down at the paragraph, "where it
   was" - the pre-fix layout) and only jumping up onto the photo once the
   transform finished easing to literal `none`. Opacity fading in over the
   same .75s made this easy to miss testing by eye, but it is there on every
   load, not just an edge case.

   The fix is the rule directly below: .research-copy's transform is pinned
   to none at all times at this width, so it can never become an unwanted
   containing block, in flight or at rest. It keeps its opacity fade-in: only
   the 30px slide is given up, mobile-only, and only for this element -
   .research-copy's #process anchor gives the override enough specificity to
   beat [data-reveal].is-visible without needing !important or depending on
   source order.

   TO REVERSE: delete this block. .research-copy's position:relative empty
   rule can also come out, but is harmless left in - it only stopped applying
   here in the first place.
   ========================================================================== */

@media (max-width: 760px) {
  .research-copy {
    position: static;
  }

  #process .research-copy {
    transform: none;
  }

  .research-heading {
    position: absolute;
    z-index: 2;
    top: 38px;
    left: 20px;
    right: 20px;
  }
}

@media (prefers-reduced-motion:reduce) {
  html {
    scroll-behavior:auto
  }
  *,:before,:after {
    transition-duration:.01ms!important;
    animation-duration:.01ms!important;
    animation-iteration-count:1!important
  }
  [data-reveal] {
    opacity:1;
    transform:none
  }
  [data-parallax] {
    transform:none
  }
}

@property --tw-outline-style {
  syntax:"*";
  inherits:false;
  initial-value:solid
}

.work-placeholder>img {
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit:cover;
  z-index:0
}

.work-placeholder>span,
.work-placeholder>strong,
.work-placeholder>a {
  position:relative;
  z-index:1
}

/* ------------------------------------------------------------------
   SELECTED WORK — full-width rows with a drifting background
   One project per row, edge to edge, no gaps. main.js maps how far the
   row has travelled through the viewport onto object-position, so the
   hero shot slides inside its frame as you scroll.
   --drift is the single tuning knob: percent either side of centre.
   45 uses almost the whole image (5% to 95%).
   ------------------------------------------------------------------ */

.work-grid {
  grid-template-columns: 1fr;
  gap: 0;
  max-width: none;
  margin-inline: calc(50% - 50vw);
}

.work-card {
  position: relative;
  grid-column: 1;
  background: var(--ink);
  border-radius: 0;
  box-shadow: none;
  overflow: hidden;
}

.work-card:hover {
  transform: none;
  box-shadow: none;
}

.work-browser {
  display: none;
}

.work-shot {
  /* FULL TRAVERSE — the top of the image as the row enters the viewport, the
     bottom as it leaves. The image box is made --travel taller than the frame
     and main.js slides it from 0 to -travel across the row's transit, so the
     whole image passes the window exactly once.

     For that to show the complete image and not a cropped band, the box has to
     be precisely as tall as the image is when drawn at full width — any less
     and object-fit:cover trims the top and bottom, any more and it crops the
     sides instead. Full-width height is 100vw / aspect, so:

         travel = 100vw / aspect - frameHeight

     which is the same maxTravel formula as before, now used as the value
     rather than as a ceiling. Set --shot-aspect per card; everything else
     follows. Pixels, not percentages — an object-position version stalls on
     narrow screens where a landscape shot has no vertical overflow to give. */
  --shot-aspect: 1.5; /* 3:2, the flat website screenshots */
  --travel: calc((100vw / var(--shot-aspect)) - clamp(360px, 46vw, 660px));
  position: relative;
  height: clamp(360px, 46vw, 660px);
  aspect-ratio: auto;
  overflow: hidden;
  background: var(--ink);
}

.work-shot img {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: calc(100% + var(--travel));
  object-fit: cover;
  object-position: 50% 50%;
  display: block;
}

.work-card:hover .work-shot img {
  transform: none;
}

/* MOCKUP CARDS — a photograph of the site on a device, rather than a flat
   screenshot. All of them are 1400x1050, so they only need their aspect
   declaring and the rule above works out the rest. Add "is-mockup" to the
   card. Upload mockups at 4:3 or squarer: the squarer the source, the taller
   it stands at full width and the further it can travel. */
.work-card.is-mockup .work-shot {
  --shot-aspect: 1.3333; /* 4:3 */
}

@media (max-width: 760px) {
  /* Phones need a fixed value, because the formula above goes negative here:
     a 4:3 image at 390px wide is only ~293px tall, shorter than the frame.
     Below this width object-fit:cover scales by height instead, so the box
     always holds the image's full height however small --travel is — the
     top-to-bottom traverse already happens, and raising this number only
     crops more width away. That crop is why it stays at 240. */
  .work-card.is-mockup .work-shot {
    --travel: 240px;
  }
}
/* Corner scrim — darkens the bottom-left of each row so the caption reads
   against any photograph. An ellipse anchored on the bottom-left corner,
   sized well past the frame so it still has weight where the caption
   actually sits (mid-left, roughly 6-45% across and 41-59% down) and has
   faded to nothing by the opposite corners. Raise the first value to
   darken; widen 140%/120% to spread it further. */
.work-card:after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(140% 120% at 0% 100%,
    rgba(16, 17, 18, .85) 0%,
    rgba(16, 17, 18, .60) 30%,
    rgba(16, 17, 18, .35) 55%,
    rgba(16, 17, 18, .12) 72%,
    rgba(16, 17, 18, 0) 88%);
}

/* Caption sits centred on the left, as on the reference. */
.work-meta {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 0 clamp(24px, 6vw, 108px);
}

.work-meta>span:first-child {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(12px, 1.4vw, 20px);
}

.work-meta b {
  color: #fff;
  font-size: clamp(30px, 5vw, 72px);
  font-weight: 700;
  letter-spacing: -.035em;
  line-height: 1;
}

.work-cta {
  color: #fff;
  font-size: clamp(13px, 1.05vw, 16px);
  letter-spacing: .02em;
  text-decoration: underline;
  text-underline-offset: 6px;
  text-decoration-thickness: 1px;
  opacity: .85;
  transition: opacity .25s;
}

.work-card:hover .work-cta {
  opacity: 1;
}

.work-meta .arrow {
  display: none;
}

@media (max-width: 760px) {
  .work-shot {
    --travel: 240px;
    height: clamp(400px, 64vh, 560px);
  }

  /* WHICH SLICE OF THE PHOTO A PHONE SEES.
     A 4:3 photo can't fit a tall narrow card, so object-fit:cover scales it
     by height and hides roughly 60% of the width. object-position decides
     which part survives, and it is purely horizontal here — the parallax
     moves the image with `top`, so panning across costs no movement at all.
     Default is centred; give a card its own value when the thing worth
     seeing isn't in the middle. 0% = show the left edge, 100% = the right. */
  .work-card.work-4 .work-shot img {
    /* Coolshed Labs: the phone stands at the far right of the frame, so a
       centred crop cut it off at its own left edge (the card showed source
       x 438-963 of 1400, and the phone starts at ~960). Right-aligning
       shows x 875-1400 — the whole phone, which is the readable half of
       this photo on a small screen, plus the right of the monitor. */
    object-position: 100% 50%;
  }
}

.work-heading {
  margin-bottom: clamp(52px, 5vw, 92px);
}

/* Honeypot field — off-screen for people, irresistible to bots. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
/* ==========================================================================
   EASY EDITING — the editor illustration inside #easy-editing.
   Deployed from the approved banner prototype, 2026-08-13.

   Every selector is scoped to .admin-demo and every new class is prefixed
   "ed-", so none of this can reach the rest of the site. The section shell,
   heading and left-hand column are still the site's own .admin-demo /
   .admin-intro rules — only the panel on the right changed, from a 1.3 MB
   screenshot to drawn CSS. Breakpoints match the site's existing 1080/760,
   not the prototype's 1060/690, so this column collapses in step with
   everything else on the page.
   ========================================================================== */

.admin-demo {
  /* The prototype shipped its own yellow (#ffc83d). It uses the site's
     --yellow here instead, so the drawn arrows match the "See it live."
     sitting directly above them — two near-identical yellows in one
     section reads as a mistake. */
  --ed-accent: var(--yellow);
  --ed-accent-soft: rgba(255, 212, 71, .14);
  --ed-line: rgba(255, 255, 255, .15);
  --ed-dim: #b7b7b2;
}

.admin-demo .admin-graphic {
  min-height: 510px;
  padding: 26px;
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: 24px;
  background:
    radial-gradient(circle at 66% 31%, rgba(255, 212, 71, .09), transparent 31%),
    linear-gradient(145deg, #171816 0%, #111210 100%);
  box-shadow: 0 35px 90px rgba(0, 0, 0, .38);
}

/* The three steps ------------------------------------------------------- */

.admin-demo .ed-process {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin: 0 0 22px;
  padding: 0;
  list-style: none;
}

.admin-demo .ed-process li {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  color: var(--ed-dim);
  font-size: 11px;
  font-weight: 650;
  letter-spacing: .04em;
  white-space: nowrap;
}

.admin-demo .ed-step {
  width: 25px;
  height: 25px;
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  border: 1px solid rgba(255, 212, 71, .52);
  border-radius: 50%;
  color: var(--ed-accent);
  background: var(--ed-accent-soft);
  font-size: 10px;
}

/* The editing surface --------------------------------------------------- */

.admin-demo .ed-workspace {
  position: relative;
  min-height: 288px;
  padding: 62px 30px 30px;
  border: 1px solid var(--ed-line);
  border-radius: 18px;
  background: rgba(255, 255, 255, .025);
  box-shadow: inset 0 0 45px rgba(255, 212, 71, .025);
}

.admin-demo .ed-mode {
  position: absolute;
  top: 18px;
  left: 22px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border: 1px solid rgba(255, 212, 71, .28);
  border-radius: 999px;
  color: #d4d3cd;
  background: rgba(255, 212, 71, .07);
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.admin-demo .ed-mode:before {
  width: 7px;
  height: 7px;
  content: "";
  border-radius: 50%;
  background: var(--ed-accent);
  box-shadow: 0 0 0 5px rgba(255, 212, 71, .1);
}

/* The serif and the dashed rule are what make this read as "your words go
   here" rather than as finished copy. */
.admin-demo .ed-editable {
  position: relative;
  display: flex;
  min-height: 120px;
  align-items: center;
  padding: 26px 74px 26px 28px;
  border: 1px dashed rgba(255, 212, 71, .48);
  border-radius: 12px;
  background: rgba(255, 255, 255, .025);
  color: #f0efe9;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(34px, 4vw, 51px);
  font-weight: 400;
  letter-spacing: -.035em;
  line-height: 1;
  box-shadow: 0 0 0 4px rgba(255, 212, 71, .025);
}

.admin-demo .ed-pencil {
  position: absolute;
  right: 23px;
  top: 14px;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  transform: rotate(-5deg);
  border: 1px solid rgba(255, 212, 71, .34);
  border-radius: 10px;
  color: var(--ed-accent);
  background: var(--ed-accent-soft);
  font-family: ui-sans-serif, sans-serif;
  font-size: 18px;
}

.admin-demo .ed-autosave {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 17px 4px 0;
  color: #aaa9a3;
  font-size: 12px;
}

.admin-demo .ed-tick {
  width: 18px;
  height: 18px;
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  border-radius: 50%;
  color: #181916;
  background: var(--ed-accent);
  font-size: 11px;
  font-weight: 900;
}

/* Pointer out to the editable line. It overhangs the workspace by 20px,
   which lands inside the panel's 26px padding — so .admin-graphic can keep
   its overflow:hidden without clipping the arrow. */
.admin-demo .ed-note {
  position: absolute;
  top: 117px;
  right: -20px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  color: var(--ed-accent);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .05em;
  line-height: 1.35;
  text-transform: uppercase;
}

.admin-demo .ed-note span {
  max-width: 98px;
}

.admin-demo .ed-note-arrow {
  width: 28px;
  height: 62px;
  position: relative;
  margin-top: 3px;
  border-top: 2px solid var(--ed-accent);
  border-right: 2px solid var(--ed-accent);
  border-radius: 0 9px 0 0;
}

.admin-demo .ed-note-arrow:after {
  position: absolute;
  right: -5px;
  bottom: -1px;
  width: 9px;
  height: 9px;
  content: "";
  transform: rotate(45deg);
  border-right: 2px solid var(--ed-accent);
  border-bottom: 2px solid var(--ed-accent);
}

/* The control bar ------------------------------------------------------- */

.admin-demo .ed-control-wrap {
  position: relative;
  margin-top: 20px;
  padding-top: 48px;
}

.admin-demo .ed-control-label {
  position: absolute;
  top: 3px;
  left: 30px;
  display: flex;
  align-items: center;
  gap: 13px;
  color: var(--ed-accent);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.admin-demo .ed-control-arrow {
  width: 73px;
  height: 29px;
  position: relative;
  flex: 0 0 auto;
  border-left: 2px solid var(--ed-accent);
  border-bottom: 2px solid var(--ed-accent);
  border-radius: 0 0 0 9px;
}

.admin-demo .ed-control-arrow:after {
  position: absolute;
  right: -1px;
  bottom: -5px;
  width: 9px;
  height: 9px;
  content: "";
  transform: rotate(-45deg);
  border-right: 2px solid var(--ed-accent);
  border-bottom: 2px solid var(--ed-accent);
}

.admin-demo .ed-control-bar {
  display: flex;
  align-items: center;
  gap: 9px;
  min-height: 58px;
  padding: 9px 11px;
  border: 1px solid rgba(255, 255, 255, .16);
  border-radius: 16px;
  background: #1b1c1a;
  box-shadow: 0 18px 28px rgba(0, 0, 0, .22);
}

.admin-demo .ed-control-tab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-right: auto;
  padding: 10px 12px;
  border-radius: 10px;
  color: #f2f0e9;
  background: var(--ed-accent-soft);
  font-size: 12px;
  font-weight: 650;
}

.admin-demo .ed-control-tab:before {
  width: 8px;
  height: 8px;
  content: "";
  border-radius: 50%;
  background: var(--ed-accent);
  box-shadow: 0 0 0 4px rgba(255, 212, 71, .12);
}

.admin-demo .ed-control-button {
  display: inline-flex;
  align-items: center;
  padding: 9px 12px;
  border: 1px solid rgba(255, 255, 255, .13);
  border-radius: 9px;
  color: #aaa9a3;
  background: transparent;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  white-space: nowrap;
}

@media (max-width: 1080px) {
  /* .admin-demo has already collapsed to one column by here, so the panel
     stops being a narrow sidebar and can lose its fixed height. */
  .admin-demo .admin-graphic {
    min-height: 0;
  }
}

@media (max-width: 760px) {
  .admin-demo .admin-graphic {
    padding: 18px;
    border-radius: 18px;
  }

  .admin-demo .ed-process {
    row-gap: 12px;
  }

  .admin-demo .ed-process li {
    align-items: flex-start;
    white-space: normal;
  }

  .admin-demo .ed-workspace {
    min-height: 0;
    padding: 60px 16px 22px;
  }

  .admin-demo .ed-editable {
    min-height: 105px;
    padding: 22px 60px 22px 20px;
  }

  /* No room for the overhanging pointer once the panel is full-bleed. */
  .admin-demo .ed-note {
    display: none;
  }

  .admin-demo .ed-control-wrap {
    padding-top: 58px;
  }

  .admin-demo .ed-control-label {
    left: 10px;
    align-items: flex-end;
  }

  .admin-demo .ed-control-label span {
    max-width: 170px;
  }

  .admin-demo .ed-control-arrow {
    width: 35px;
  }

  .admin-demo .ed-control-bar {
    flex-wrap: wrap;
  }

  .admin-demo .ed-control-tab {
    width: 100%;
  }

  .admin-demo .ed-control-button {
    flex: 1 1 auto;
    justify-content: center;
  }
}

/* ==========================================================================
   NIGHT HERO — 2026-08-13.

   The hero photograph changed from a daylit street to a night one, so every
   piece of the hero that was drawn for a white background is inverted here.
   The rules above are left exactly as they were, and this block overrides
   them, so switching back to a light hero means deleting this block and
   putting /hero-big-dream-v1.webp back in index.php — nothing else.

   The flat colour behind the photo is var(--ink) #101112, which is within a
   point of the image's own darkest edge (sampled at #111111), so the join
   between photo and background is invisible.
   ========================================================================== */

.hero {
  background: var(--ink);
  /* Cascades to the h1, intro, eyebrow, scroll cue and text link. Anything
     that needs its own colour on top of this sets it below. */
  color: var(--paper);
}

/* The bottom fade used to dissolve the hero into the cream page. It now
   settles into solid --ink instead, and the light .promise section starts
   on a hard edge — the same way .free-offer meets .contact further down. */
.hero:after {
  background: linear-gradient(180deg,
    rgba(16, 17, 18, 0) 0%,
    rgba(16, 17, 18, .12) 20%,
    rgba(16, 17, 18, .68) 62%,
    var(--ink) 100%);
}

/* ZOOMED OUT — the photo spans the whole hero instead of starting at 34%.
   The old frame was very nearly square (950x937 at a 1440 viewport) while
   the photograph is 16:9, so object-fit:cover had to throw away about 43%
   of its width to fill it. Full width takes that down to roughly 14%.
   Scoped to min-width:761px because the mobile rules further up give
   .hero-image a completely different inset, and this block sits after
   them in the file — unscoped, it would win there too and break it. */
@media (min-width: 761px) {
  .hero-image {
    inset: 0 0 -48px 0;
  }

  /* The wash that keeps the copy legible now has more ground to cover than
     the 34% version did, but it can stay gentle because the pixels beneath
     it are the unlit end of the street already. The neon half of the
     photograph is left completely alone. */
  .hero-image:after {
    background:
      linear-gradient(90deg,
        rgba(16, 17, 18, .92) 0%,
        rgba(16, 17, 18, .78) 18%,
        rgba(16, 17, 18, .42) 38%,
        rgba(16, 17, 18, .10) 56%,
        transparent 72%),
      linear-gradient(rgba(16, 17, 18, .12), transparent 45%);
  }
}

.hero .eyebrow {
  color: rgba(247, 247, 243, .72);
}

/* This rule USED to be "only the stroke colour changes" — a --ink stroke was
   invisible on the black hero, so it was repainted in --paper. The stroke
   itself was dropped on 2026-08-26 (see -webkit-text-stroke:0 in the base
   .hero h1 em rule), so the declaration below is now a no-op, commented out
   and kept only as the other half of that reverse instruction. */
.hero h1 em {
  /* -webkit-text-stroke-color: var(--paper); */

  /* ---- SHOPFRONT GRADIENT, ANIMATED -------------------------------------
     TO REVERSE: delete from here to the end of this rule and put back the
     single static declaration kept in the comment at the bottom of this
     block. Nothing else in the file depends on it.

     These are not invented colours — they were sampled out of the hero
     photograph itself, by quantising the shopfront region (x840-1540,
     y330-750) down to twelve colours and taking the ones that carry it:

         #9A2E0A  burnt orange, the deep shadow inside the doorway
         #E3610D  the orange of the open door
         #F69C11  the amber window glow
         #EFC74F  the warm light spilling onto the stone
         #29B4CF  the turquoise of the right-hand window
         #12649E  the deep blue behind it

     They run warm-to-cool left to right, the same way the shopfront does,
     and the sequence is a palindrome — it ends on the colour it starts on —
     so the tile repeats with no seam when it moves. */
  background-image: linear-gradient(90deg,
    #9A2E0A 0%,
    #E3610D 12%,
    #F69C11 24%,
    #EFC74F 36%,
    #29B4CF 50%,
    #12649E 64%,
    #29B4CF 76%,
    #E3610D 88%,
    #9A2E0A 100%);

  /* Twice the width of the text and tiled, so there is always more gradient
     waiting off both edges. Shifting the position by exactly 200% moves it
     by one whole tile, which is what makes the loop invisible. */
  background-size: 200% 100%;
  background-repeat: repeat-x;
  /* 16s originally, which read as almost static. This is the pace dial. */
  animation: hero-em-drift 10s linear infinite;
}

@keyframes hero-em-drift {
  from { background-position: 0% center; }
  to   { background-position: 200% center; }
}

/* Movement is decoration here, so it goes away for anyone who has asked
   their system for less of it. The colours stay. */
@media (prefers-reduced-motion: reduce) {
  .hero h1 em {
    animation: none;
  }
}

/* PREVIOUS STATIC FILL, kept for the one-line reverse described above:
   background-image: linear-gradient(90deg, rgba(137,48,190,.88) 0%,
     rgba(231,58,145,.88) 18%, rgba(255,78,35,.88) 34%,
     rgba(255,137,0,.88) 45%, rgba(255,194,24,.88) 55%,
     rgba(0,190,204,.88) 68%, rgba(0,129,225,.88) 82%,
     rgba(0,193,218,.88) 100%);                                          */

.hero-intro {
  color: rgba(247, 247, 243, .78);
}

.hero .text-link {
  border-bottom-color: rgba(247, 247, 243, .5);
}

/* The £150 badge inverts: a dark disc disappeared against the night, and it
   sits over the lit pavement, so white with a dark ring is what separates
   it from the neon behind. */
/* The £150 badge. Its ring runs the shopfront's spectrum the whole way round
   — the same six sampled colours as the headline gradient and the pointer
   trail, laid out as a cone so they wrap the circle.

   Done as a transparent border with two backgrounds rather than a border-image
   or a pseudo-element: the cone is clipped to the border box and the flat
   paper fill to the padding box, which leaves exactly the border width showing
   as a ring. Note the colour list is a palindrome — a cone joins its last stop
   back to its first, so anything else would leave a hard seam at the top.
   box-sizing is border-box site-wide, so the badge stays 166px overall and the
   ring is taken out of the inside. */
.hero-proof {
  color: var(--ink);
  border: 8px solid transparent;
  background-color: transparent;
  background-image:
    linear-gradient(var(--paper), var(--paper)),
    conic-gradient(from 200deg,
      #9A2E0A 0%,
      #E3610D 11%,
      #F69C11 22%,
      #EFC74F 33%,
      #29B4CF 46%,
      #12649E 56%,
      #29B4CF 68%,
      #F69C11 80%,
      #E3610D 90%,
      #9A2E0A 100%);
  background-origin: padding-box, border-box;
  background-clip: padding-box, border-box;
  box-shadow: 0 0 0 3px rgba(16, 17, 18, .45), 0 18px 50px rgba(0, 0, 0, .45);
}

@media (max-width: 760px) {
  /* 8px reads as a heavy band once the badge drops to 116px. */
  .hero-proof {
    border-width: 6px;
  }
}

.hero .scroll-cue i {
  background: var(--paper);
}

/* Header over the hero -----------------------------------------------------
   The bar is transparent until .is-stuck adds the frosted background past
   the hero, so light type applies only while it is NOT stuck. The second
   guard matters just as much: the mobile menu overlay is cream, so its
   links must stay dark while it is open. */
.site-header:not(.is-stuck):not(:has(nav.nav-open)) {
  color: var(--paper);
}

.site-header:not(.is-stuck):not(:has(nav.nav-open)) nav > a:not(.nav-cta):after {
  background: var(--paper);
}

.site-header:not(.is-stuck):not(:has(nav.nav-open)) .nav-cta {
  color: var(--ink);
  background: var(--paper);
}

.site-header:not(.is-stuck):not(:has(nav.nav-open)) .menu-toggle span {
  background: var(--paper);
}

@media (max-width: 760px) {
  /* Phones crop hardest of all: a 390-wide block 520 tall is portrait, and
     a 16:9 photo filling it loses about 61% of its width. Shortening the
     block is the only lever that doesn't letterbox — every 80px off it
     buys roughly 7% more of the picture. */
  .hero-image {
    height: 440px;
  }

  /* On phones the photo drops below the copy, so its wash runs top-down
     into the text rather than left-to-right. */
  .hero-image:after {
    background:
      linear-gradient(var(--ink) 0, rgba(16, 17, 18, .83) 15%, transparent 45%),
      linear-gradient(90deg, rgba(16, 17, 18, .42), transparent);
  }
}

/* The canvas the hero pointer trail draws on. Created by main.js, so it is
   absent unless that block runs. z-index 3 puts it above the photograph but
   below the bottom fade (4) and the copy (5) — the sparks pass behind the
   headline, which keeps the type crisp and reads as light behind the words. */
.hero-trail {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* ==========================================================================
   HERO CURTAIN — 2026-08-13.
   ========================================================================== */

/* The intro curtain — TWO panels, both white and both lifting upwards. Panel
   one is solid white and HOLDS FOR 1.2 SECONDS with the Skyline wordmark
   centred on it (shortened from a 2s hold, 2026-08-20), then wipes upwards,
   taking the wordmark with it because the mark is a child of the panel
   rather than a separate layer.
   Panel two follows: white at 50%, so the photograph shows through it as a
   haze, carrying the amber seam along its bottom edge — that light line is
   what makes the movement read, and it is on the last panel because that is
   the edge crossing the picture last.

   Panel two starts a fifth of a second behind panel one and takes longer, so
   its bottom edge always trails below panel one's and the seam is never
   hidden by it. Rising upwards means the brightest part of the picture, the
   lit pavement, arrives first.

   z-index 30/31 puts both panels ABOVE the fixed site header (z-index 20, and
   22 for the mobile nav). That matters during the hold: the header type
   is white over the hero, so without this the nav would sit invisibly on the
   white panel for that time. `.hero` is position:relative with no z-index of
   its own, so it makes no stacking context and these values compete with the
   header's directly.

   The resting state of both is already lifted, and only the animation puts
   them back down over the hero. That way a browser that ignores the
   animation, or a visitor who has asked for less motion, simply never sees a
   curtain — neither panel can get stuck covering the hero. */
.hero .hero-curtain {
  position: absolute;
  inset: 0;
  pointer-events: none;
  transform: translateY(-100%);
}

/* Panel one: flat white, no seam, and the stage for the wordmark. White
   lifting off a night photograph is already the strongest edge on the page,
   so it needs no help to read.

   It sits ABOVE panel two (31 vs 30). That is the whole reason the wordmark
   reads as black: panel two is 50% white, and while both panels are down
   during the hold it would otherwise veil the mark to mid-grey. Panel two
   loses nothing by being underneath, because it both starts later and travels
   longer, so its bottom edge always trails panel one and its amber seam is
   never covered. */
.hero .hero-curtain--one {
  z-index: 31;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #fff;
}

/* Panel two: half-strength white plus the amber seam, underneath panel one. */
.hero .hero-curtain--two {
  z-index: 30;
  background:
    linear-gradient(to top,
      rgba(246, 156, 17, .9) 0px,
      rgba(246, 156, 17, .35) 4px,
      rgba(246, 156, 17, .08) 24px,
      rgba(246, 156, 17, 0) 64px),
    rgba(255, 255, 255, .5);
}

/* The wordmark — the word on its own, no "WEB AND MEDIA" strapline. It reads
   as a fast preloader: it sits grey at rest, then a second, identical copy
   in pure black (rather than the site's --ink, so it reads as hard as
   possible against the white panel) wipes over it left to right, as if a
   loading bar were filling the letters in. `.curtain-mark-fill` is that
   black copy — same markup, absolutely positioned over the grey original,
   revealed with an animated clip-path. */
.hero .curtain-mark {
  position: relative;
  display: grid;
  color: #c4c6c5;
  line-height: 1;
}

.hero .curtain-mark strong {
  display: flex;
  align-items: flex-start;
  font-size: clamp(52px, 11vw, 150px);
  font-weight: 400;
  letter-spacing: -.035em;
}

.hero .curtain-mark strong b { font-weight: 950; }
.hero .curtain-mark strong span { font-weight: 400; }

/* Resting state is fully revealed, same logic as the curtain panels above:
   a browser that skips the animation (or a visitor who prefers less motion)
   should simply see the finished black wordmark, never a stuck grey one. */
.hero .curtain-mark-fill {
  position: absolute;
  inset: 0;
  color: #000;
  clip-path: inset(0 0 0 0);
}

/* The brief hold at the start is inside the keyframes rather than being an
   animation-delay, which lets the fill mode be `forwards` only. With `both`
   and a delay, the backwards fill pins the panel over the hero before the
   animation starts — so an animation that never progresses leaves the hero
   covered for good. This way the resting state above is the only thing a
   browser can fall back to, and that state is already lifted. The stagger
   between the two is likewise a longer hold, not a delay, for the same
   reason.

   THE HOLD IS THE PERCENTAGE, so if you change a duration you must change its
   percentage to match, or the pause drifts:
     panel one  60%    of 1.00s = 0.60s held, then 0.40s of travel
     panel two  56.5%  of 1.15s = 0.65s held, then 0.50s of travel

   2026-08-27: halved. Was 1.65s / 1.92s, which held the page for nearly two
   seconds before anything appeared - a convincing preloader, but the site
   is not actually slow and the intro was the only thing making it look it.
   Both percentages recomputed from the new durations, per the warning
   above; curtain-mark-fill below was re-timed to 0.6s to keep landing
   exactly on panel one's hold.  */
@keyframes hero-curtain {
  0%, 60% { transform: translateY(0); }
  100% { transform: translateY(-100%); }
}

@keyframes hero-curtain-second {
  0%, 56.5% { transform: translateY(0); }
  100% { transform: translateY(-100%); }
}

/* The wordmark fades up as the panel settles, so the hold opens with a small
   move rather than two seconds of a static image. Delete this rule and the
   keyframes to have it simply be there from the first frame. */
@keyframes curtain-mark-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: translateY(0); }
}

/* The black copy wipes in left to right, uncovering the grey original
   beneath it — the preloader-style fill. */
@keyframes curtain-mark-wipe {
  from { clip-path: inset(0 100% 0 0); }
  to { clip-path: inset(0 0 0 0); }
}

/* The hero copy's arrival — the exact shape [data-reveal] used to give it
   (opacity plus a 30px lift), as a one-shot load animation. See the note on
   .hero .hero-copy below for why it stopped being a scroll reveal. */
@keyframes hero-copy-in {
  from { opacity: 0; transform: translateY(30px); }
  to   { opacity: 1; transform: none; }
}

/* The hero photo fades in once both curtain panels have fully lifted. This
   is a plain page-load animation, not the scroll-triggered [data-reveal]
   system — it runs exactly once and never replays if the visitor scrolls
   the hero out of view and back. */
@keyframes hero-image-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@media (prefers-reduced-motion: no-preference) {
  .hero .hero-curtain--one {
    animation: hero-curtain 1s cubic-bezier(.76, 0, .24, 1) forwards;
  }

  /* The same hold-then-lift shape as panel one, so the two read as one
     gesture, but with a much longer tail on the easing — it does most of its
     travel early and then glides to a stop rather than arriving with panel
     one's snap. */
  .hero .hero-curtain--two {
    animation: hero-curtain-second 1.15s cubic-bezier(.62, 0, .08, 1) forwards;
  }

  /* Delayed to start exactly when panel two's travel ends, then a slow
     1.5s fade so the photo settles in smoothly rather than snapping in. */
  .hero-image {
    animation: hero-image-in 1s cubic-bezier(.16, 1, .3, 1) 1.15s both;
  }

  .hero .curtain-mark {
    animation: curtain-mark-in .24s cubic-bezier(.16, 1, .3, 1) .04s both;
  }

  /* The preloader-style fill: 1.2s linear, starting immediately, so it lands
     exactly on panel one's 0.60s hold (see THE HOLD IS THE PERCENTAGE
     above) — a steady, unaccelerated sweep, like a loading bar, finishing
     right as the panel starts to lift. */
  .hero .curtain-mark-fill {
    animation: curtain-mark-wipe .6s linear both;
  }

  /* The copy fades in on the curtains' heels rather than being revealed
     already finished.

     2026-08-26 — THIS IS NOW A LOAD ANIMATION, NOT A [data-reveal].
     It used to be `--delay: 1.56s` feeding the [data-reveal] transition, and
     .hero-copy carried data-reveal="true" in index.php. That made the hero's
     appearance depend on an IntersectionObserver, which is wrong for the one
     block of copy that is always above the fold: the observer's first
     callback can be deferred or missed (a backgrounded tab, a restored
     session, a slow first frame), and the copy then sat at opacity 0 until
     something forced a fresh intersection calculation - scrolling back to the
     top - at which point it played the full 1.56s intro all over again, long
     after the page had loaded. Reported by the user as "scroll to the bottom
     and back up and the hero text appears after a few seconds, as if the
     intro is being used".

     A plain load animation cannot do either: it runs once, when the page
     loads, and there is nothing to re-trigger. Timing, easing, delay and the
     30px lift are copied exactly from the [data-reveal] rule it replaces, so
     the first-load look is unchanged.

     Same fallback logic as the curtain panels above: with data-reveal gone
     there is no opacity:0 base rule, so the resting state is the FINISHED
     one. A browser that skips the animation, or a visitor who has asked for
     less motion, sees the copy immediately rather than never.
     TO REVERSE: put data-reveal="true" back on .hero-copy in index.php and
     swap the animation below for `--delay: 1.56s`. */
  .hero .hero-copy {
    animation: hero-copy-in .75s cubic-bezier(.2, .8, .2, 1) 1.56s both;
  }
}

/* ==========================================================================
   PRICING — NIGHT THEME. Deployed from the approved prototype, 2026-08-13.

   Every selector is scoped to .pricing, and the markup was not touched at all:
   the existing cards already carried .launch / .elevate / .signature and the
   right structure, so the prototype's look is applied over them. That keeps
   the wording, prices, features, hosting lines, #contact links and the reveal
   delays exactly as they were.

   The light-theme rules further up the file are left in place and overridden
   here, so reverting is: delete this block. Selectors are pitched at
   .pricing .price-card ... deliberately — the old .price-card.signature
   overrides are three classes deep, and anything weaker loses to them.
   ========================================================================== */

.pricing {
  /* The prototype's palette, kept local so none of it leaks out of the
     section. --ink / --paper are the site's own and stay untouched. */
  --pc-card: #121314;
  --pc-raised: #151719;
  --pc-cyan: #43d9e7;
  --pc-amber: #ffbd45;
  --pc-magenta: #e35b9d;
  --pc-muted: #a9aaab;

  background: #0c0d0e;
  color: var(--paper);
}

/* Was two small light-tinted corner washes; now the prototype's cool glow
   rising from beneath the cards, plus a trace of the old two. */
.pricing:before {
  background:
    radial-gradient(circle at 52% 115%, rgba(52, 182, 205, .07), transparent 40%),
    radial-gradient(circle at 8% 19%, rgba(67, 217, 231, .05), transparent 18%),
    radial-gradient(circle at 92% 8%, rgba(255, 189, 69, .05), transparent 17%);
}

.pricing .section-label,
.pricing .section-heading h2 {
  color: var(--paper);
}

/* The label's leading dash is drawn with currentColor, so it follows along. */
.pricing .section-heading h2 span {
  color: #9aa0a0;
}

.pricing .section-heading > p:last-child,
.pricing .pricing-footnote {
  color: var(--pc-muted);
}

/* THE CARDS ------------------------------------------------------------- */

.pricing .price-card {
  --tone-a: var(--pc-cyan);
  --tone-b: var(--pc-amber);
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 656px;
  color: #d4d4d0;
  border: 1px solid rgba(255, 255, 255, .16);
  border-radius: 25px;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .025), transparent 22%),
    var(--pc-card);
  /* Plain shadow first, then the tinted version — a browser without
     color-mix() drops the whole declaration, so it needs something to fall
     back to rather than nothing. */
  box-shadow: 0 24px 80px rgba(0, 0, 0, .28), inset 0 1px 0 rgba(255, 255, 255, .035);
  box-shadow:
    0 24px 80px rgba(0, 0, 0, .28),
    0 0 32px color-mix(in srgb, var(--tone-a), transparent 92%),
    inset 0 1px 0 rgba(255, 255, 255, .035);
  isolation: isolate;
}

.pricing .price-card:hover {
  transform: translateY(-8px);
  box-shadow:
    0 34px 90px rgba(0, 0, 0, .4),
    0 0 44px color-mix(in srgb, var(--tone-a), transparent 86%),
    inset 0 1px 0 rgba(255, 255, 255, .05);
}

/* The two glows - both --tone-a now, so top and bottom read as the same
   light rather than two different-coloured ones. They used to be tone-a on
   top and tone-b on the bottom (a second accent colour only otherwise used
   for the diagonal wash on the button's hover state, see .button-card:before
   below) - on Elevate that meant a cyan top and a magenta bottom, which read
   as a mismatch rather than one card glowing from two directions. Sizes and
   opacity increased a little at the same time, so the card's colour is more
   present without tipping past a glow into a solid shape.

   z-index:-1 inside the card's own stacking context paints them over its
   background but under the text, which is what makes them read as light in
   the card rather than as a shape on top of it. overflow:hidden on the card
   crops them to its rounded corners. */
.pricing .price-card:before {
  content: "";
  position: absolute;
  z-index: -1;
  top: -90px;
  right: -55px;
  width: 280px;
  height: 280px;
  opacity: .34;
  border-radius: 50%;
  background: radial-gradient(circle, var(--tone-a), transparent 68%);
  filter: blur(13px);
}

.pricing .price-card:after {
  content: "";
  position: absolute;
  z-index: -1;
  right: 12%;
  bottom: -55px;
  left: 12%;
  height: 130px;
  opacity: .3;
  background: radial-gradient(ellipse, var(--tone-a), transparent 70%);
  filter: blur(14px);
}

.pricing .price-card.launch {
  --tone-a: #a7c9cf;
  --tone-b: var(--pc-amber);
  border-color: rgba(167, 201, 207, .34);
}

.pricing .price-card.elevate {
  --tone-a: var(--pc-cyan);
  --tone-b: var(--pc-magenta);
  border-color: rgba(67, 217, 231, .43);
  background:
    linear-gradient(145deg, rgba(67, 217, 231, .085), transparent 46%),
    var(--pc-raised);
}

.pricing .price-card.signature {
  --tone-a: var(--pc-amber);
  --tone-b: var(--pc-cyan);
  color: #d4d4d0;
  border-color: rgba(255, 189, 69, .46);
  background:
    linear-gradient(145deg, rgba(255, 189, 69, .1), transparent 39%),
    #101112;
}

/* CARD INTERNALS -------------------------------------------------------- */

/* The body becomes the flex column so the hosting note and button can be
   pinned to the bottom of every card regardless of how many features it has,
   which is what keeps the three CTAs on one line across the row. */
.pricing .price-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  padding: 30px 28px;
}

.pricing .price-top p {
  margin: 0 0 13px;
  color: #f6f5f0;
  font-size: 10px;
  letter-spacing: .22em;
}

/* The dot in front of the package name, glowing in that card's own tone. */
.pricing .price-top p:before {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin: 0 9px 1px 0;
  border-radius: 50%;
  background: var(--tone-a);
  box-shadow: 0 0 12px color-mix(in srgb, var(--tone-a), transparent 35%);
}

.pricing .price-top h3 {
  max-width: 310px;
  color: #eeeeeb;
  font-weight: 400;
  letter-spacing: -.035em;
  line-height: 1.2;
}

/* "From" moves onto its own line above the figure, as in the prototype. */
.pricing .price {
  display: block;
  margin: 42px 0 0;
  color: #fff;
  font-size: clamp(52px, 4vw, 59px);
  letter-spacing: -.065em;
}

.pricing .price small {
  display: block;
  margin: 0 0 -3px;
  color: #c7c6c2;
  font-size: 9px;
  letter-spacing: .2em;
}

.pricing .price-note {
  min-height: 43px;
  margin: 13px 0 0;
  color: var(--pc-muted);
}

/* The prototype's gradient rule, drawn as a 1px background on the top edge of
   the feature list — the site's markup has no <hr> to style and this needs no
   markup change. It starts in the card's tone and fades to plain white. */
.pricing .price-card ul {
  margin: 30px 0 0;
  padding: 25px 0 0;
  border-top: 0;
  background-image: linear-gradient(90deg, var(--tone-a), rgba(255, 255, 255, .1) 21%, rgba(255, 255, 255, .1));
  background-repeat: no-repeat;
  background-size: 100% 1px;
  background-position: left top;
}

.pricing .price-card li {
  margin: 0 0 13px;
  color: #d4d4d0;
  font-size: 13px;
  line-height: 1.3;
}

.pricing .price-card li span {
  color: var(--tone-a);
}

/* margin-top:auto is what drops this pair to the foot of the card. */
.pricing .price-card .hosting {
  margin-top: auto;
  min-height: 66px;
  padding: 14px 15px;
  color: #989999;
  border: 1px solid rgba(255, 255, 255, .065);
  border-radius: 11px;
  background: rgba(255, 255, 255, .045);
}

.pricing .price-card .hosting span {
  color: #d8d7d3;
  font-size: 9px;
  letter-spacing: .18em;
}

/* THE BUTTONS — white on every card, including Signature, which used to be
   yellow, and Elevate, which used to be teal. */
.pricing .price-card .button-card {
  position: relative;
  overflow: hidden;
  min-height: 52px;
  margin-top: 19px;
  padding: 0 22px;
  color: #121313;
  border: 1px solid rgba(255, 255, 255, .13);
  background: #f7f6f2;
  /* The lift is on `translate`, not `transform` — see the reveal note at the
     end of this file, which owns `transform` on these buttons. */
  transition: border-color .18s ease, background .18s ease, translate .18s ease;
}

/* A wash of the card's own two tones across the button, barely there until
   it is hovered or focused. */
.pricing .price-card .button-card:before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: .075;
  background: linear-gradient(100deg, var(--tone-a), transparent 40%, var(--tone-b));
  transition: opacity .18s ease;
}

.pricing .price-card .button-card .arrow {
  position: relative;
  color: #121313;
  font-size: 17px;
}

.pricing .price-card .button-card:hover,
.pricing .price-card .button-card:focus-visible {
  translate: 0 -2px;
  border-color: #fff;
  background: #fff;
  box-shadow: 0 8px 24px rgba(255, 255, 255, .09);
  outline: none;
}

.pricing .price-card .button-card:hover:before,
.pricing .price-card .button-card:focus-visible:before {
  opacity: .18;
}

/* Keyboard users still get a visible ring, since the hover styling alone is
   too subtle to serve as a focus indicator on its own. */
.pricing .price-card .button-card:focus-visible {
  box-shadow: 0 0 0 3px rgba(67, 217, 231, .55), 0 8px 24px rgba(255, 255, 255, .09);
}

@media (max-width: 1050px) {
  .pricing .price-card {
    min-height: 0;
  }
}

@media (max-width: 540px) {
  .pricing .price-body {
    padding: 25px 21px;
  }

  .pricing .price-card {
    border-radius: 20px;
  }

  .pricing .price {
    margin-top: 34px;
  }
}

/* ==========================================================================
   PORTFOLIO INTO PRICING, AND EASY EDITING BACK TO LIGHT — 2026-08-13.
   ========================================================================== */

/* The portfolio's own bottom padding was a band of #e9eae5 between the last
   full-bleed card and the pricing section. Rather than paint that band black
   and have to keep a magic number in step with .section-pad, the padding is
   simply dropped: the last card now sits straight on top of #pricing, whose
   existing 120px top padding (82px on mobile) becomes the gap and is already
   black. Same spacing as before, no seam, and it stays right at every width
   because it is the pricing section's own padding doing the work. */
.work {
  padding-bottom: 0;
}

/* EASY EDITING — DARK AGAIN, 2026-08-26 ------------------------------------
   Full history, because this section has now been both colours twice:
   it was designed dark, flipped to light when #pricing went dark (two dark
   sections were running together), and is dark again now that it has moved
   out from behind #pricing to sit between #free-website and #faq — both #fff,
   so light made three pale sections in a row.

   There is no dark rule to add. The BASE `.admin-demo` rule (search
   "background:var(--ink)") is the original dark treatment and was never
   removed — background --ink, color #fff, `.light-label` #afb2b0,
   `.admin-intro > p:last-child` #bcc0bd. The light version was four override
   rules sitting here; deleting them is the whole change, and the section
   falls back to what it was always meant to look like.

   One of those four (`.admin-demo .admin-intro h2 span { color: #8a8d8b }`)
   had already been dead since the gradient subheadings landed — see the
   !important note in the SUBHEADING GRADIENT block.

   TO GO LIGHT AGAIN: put back
     .admin-demo { color: var(--ink); background: var(--paper); }
     .admin-demo .light-label { color: var(--ink); }
     .admin-demo .admin-intro > p:last-child { color: #5c605e; }  */

/* ==========================================================================
   HERO FILLS THE SCREEN — 2026-08-13.

   It never actually did. The base rule was height:min(900px,100vh) with a
   820px floor, so a tall device got a 900px hero with the next section
   showing beneath it (measured: 900 on a 1024x1366 iPad, 900 on 820x1180),
   while mobile was a flat min-height:780px — TALLER than a 375x667 phone,
   so the hero overflowed the screen there instead of fitting it.
   ========================================================================== */

.hero {
  /* svh, not vh: on phones vh is the height with the browser chrome hidden,
     so a 100vh hero is always taller than what you can actually see and the
     bottom is pushed under the toolbar. svh is the chrome-showing height, so
     the hero fits on first paint and never shifts. vh first as the fallback. */
  height: 100vh;
  height: 100svh;
  /* Only a floor for genuinely tiny windows, where filling the screen matters
     less than the copy still fitting. */
  min-height: 560px;
}

@media (max-width: 1080px) {
  .hero {
    min-height: 560px;
  }
}

@media (max-width: 760px) {
  .hero {
    /* Was height:auto + min-height:780px. */
    height: 100vh;
    height: 100svh;
    min-height: 560px;
    padding: 96px 20px 88px;
  }

  /* The photo was pinned 265px from the top at a fixed 440px tall, which ran
     off the bottom of any phone shorter than about 700px. Anchoring it to the
     foot of the hero and sizing it in percent means it scales with whatever
     the screen actually is. */
  .hero-image {
    top: auto;
    bottom: 0;
    height: 48%;
  }

  /* "Made visible" needed 386px of line at the old 78px and had 335px on a
     390px phone, so it broke in two. The em is 10px smaller than the h1 and
     this face needs about 5.68px of width per px of font-size (measured on
     Arial Black; the h1 moved to Arial 700 on 2026-08-26, which is NARROWER,
     so this ceiling is now conservative rather than wrong), which puts the
     ceiling at (viewport - 55) / 5.68 — roughly 17vw once the -10px is added
     back. Clamped so it neither disappears on a 320px phone nor overshoots
     approaching the 760px breakpoint. */
  .hero h1 {
    font-size: clamp(44px, 17vw, 74px);
  }
}

/* ==========================================================================
   HERO VERTICAL SPACING — 2026-08-13.

   Measured gap between the fixed header and the top of the copy, before this:
     desktop 1440x900    165px   (fine)
     iPad Air  820x1180  362px   \ copy stranded in the middle of a very
     iPad Pro 1024x1366  444px   / tall hero, with a void above it
     Galaxy S22 360x780   22px   almost touching the header

   The cause is one rule doing two jobs. align-items:center is right on a
   landscape screen but on a portrait tablet it centres the copy in 1366px of
   height, so the gap grows with the screen; on phones the layout is
   top-aligned with a fixed padding, so the gap does not grow at all. Both are
   handled below, and landscape desktop is deliberately left alone.
   ========================================================================== */

/* Tall screens wider than a phone — portrait tablets, and any window taller
   than 3:4. Top-align rather than centre, so the copy sits in the upper third
   and the photograph fills the space beneath it instead of the copy floating
   in the middle of nowhere. */
@media (min-width: 761px) and (max-aspect-ratio: 4 / 3) {
  .hero {
    align-items: flex-start;
    /* 84px of that is the header itself. Bounded so it cannot run away on a
       very tall screen the way centring did. */
    padding-top: clamp(180px, 15vh, 250px);
  }
}

@media (max-width: 760px) {
  .hero {
    /* Was 96px, which left only 22px between the 74px header and the eyebrow
       — fine on a roomy iPhone, cramped on a shorter phone like the S22.
       Scales gently with height, with a floor that keeps short phones clear
       of the header. */
    padding-top: clamp(128px, 16vh, 152px);
  }

  /* Trimmed from 48% to make room for the copy now sitting a little lower,
     so the buttons are not pushed deep into the photograph. */
  .hero-image {
    height: 44%;
  }
}

/* ==========================================================================
   NARROW PHONES — 2026-08-14.

   A real iPhone 8 (375x667) showed the £150 badge sitting on top of the
   "See clear pricing" button. Chrome's emulator at the same 375x667 showed
   them clear, so the emulator was wrong and the device was right: iOS falls
   back to different fonts (there is no Arial Black on iOS, and its Arial has
   its own metrics), every string comes out a little wider, and the eyebrow
   tipped onto a second line — which pushed the whole copy block down into the
   badge. Anything tuned to exact pixel positions from an emulator will keep
   breaking that way.

   So the fix is structural rather than positional: the button and the badge
   are made unable to overlap HORIZONTALLY, which means it no longer matters
   how far down the page the copy pushes them.
   ========================================================================== */

@media (max-width: 420px) {
  /* One line, with room to spare — it was landing within ~5px of the wrap
     point, which iOS's wider metrics were enough to tip over. */
  .hero .eyebrow {
    font-size: 8.5px;
    letter-spacing: .13em;
  }

  /* A narrower pill, so its right edge stays well clear of the badge. */
  .hero-actions .button {
    gap: 14px;
    padding: 15px 18px;
  }

  /* Smaller, and tucked right into the corner. The spectrum ring scales with
     it so it does not read as a heavy band at this size. */
  .hero-proof {
    width: 92px;
    height: 92px;
    right: 14px;
    bottom: 16px;
    border-width: 5px;
    box-shadow: 0 0 0 3px rgba(16, 17, 18, .45), 0 12px 30px rgba(0, 0, 0, .45);
  }

  .hero-proof strong {
    font-size: 26px;
  }

  .hero-proof span {
    margin-top: 5px;
    font-size: 6.5px;
    letter-spacing: .1em;
  }
}

@media (max-width: 344px) {
  /* Below this there is genuinely no room for both, and the button matters
     more than the badge — the £150 is repeated in full in the pricing
     section, the call to action is not. */
  .hero-proof {
    display: none;
  }
}

/* ==========================================================================
   FOOTER LOGO — 2026-08-14.

   The markup was already an exact copy of the header's, but two footer rules
   were landing on it and taking it apart:

     footer > a { text-transform: uppercase; ... }   meant for "BACK TO TOP",
       but the logo is also a direct <a> child, so the wordmark rendered as
       "SKYLINE" instead of "Skyline".

     footer small { grid-column: 2; color: #6d716e }   meant for the copyright
       line, but it also matched the <small> inside .logo-type, pulling
       "WEB AND MEDIA" into a second column BESIDE the wordmark rather than
       stacked under it, in grey.

   Overridden here rather than by narrowing those two rules, so the copyright
   line and the back-to-top link keep exactly the styling they had.
   ========================================================================== */

footer > a.logo {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
  /* Was stretching across its whole grid column, making the entire left third
     of the footer a click target for "back to top". */
  justify-self: start;
}

footer .logo-type small {
  grid-column: auto;
  color: inherit;
}

/* ==========================================================================
   HEADER STATE TRANSITIONS — 2026-08-14.

   The bar's height and the frosted layer's opacity were already animated, but
   the pieces inside it snapped: the logo jumped 28px->23px and the CTA pill
   changed padding in a single frame, so "shrinks to a frosted bar" looked
   half-animated. These make the whole thing move together.
   ========================================================================== */

.site-header {
  /* Adds colour to the existing height/transform transition, so the switch
     from light type on the hero to dark type on the frosted bar fades rather
     than flicking over. */
  transition: height .3s ease, transform .35s ease, color .3s ease;
}

.site-header .logo-type strong {
  transition: font-size .3s ease;
}

.site-header .nav-cta {
  transition: padding .3s ease, background .3s ease, color .3s ease;
}

.site-header nav > a:not(.nav-cta):after {
  transition: right .25s ease, background .3s ease;
}

/* ==========================================================================
   PROMISE CHARACTER SEQUENCE — 2026-08-15.

   Scoped to #promise. JavaScript adds .promise-characters-ready only after it
   has safely wrapped the live text, so the section remains readable if the
   script is unavailable. Words stay intact while their characters animate.
   ========================================================================== */

.promise-characters-ready .promise-word {
  display: inline-block;
  white-space: nowrap;
}

.promise-characters-ready .promise-char {
  display: inline-block;
  opacity: 0;
  filter: blur(5px);
  transform: translate3d(0, .72em, 0) rotateX(-42deg);
  transform-origin: 50% 100%;
  transition:
    opacity .48s cubic-bezier(.2, .7, .2, 1),
    transform .7s cubic-bezier(.16, 1, .3, 1),
    filter .55s cubic-bezier(.16, 1, .3, 1);
  transition-delay: var(--char-delay, 0ms);
}

.promise-characters-ready .promise-char.is-visible {
  opacity: 1;
  filter: blur(0);
  transform: translate3d(0, 0, 0) rotateX(0);
}

@media (prefers-reduced-motion: reduce) {
  .promise-characters-ready .promise-char {
    opacity: 1;
    filter: none;
    transform: none;
    transition: none;
  }
}

/* PROMISE BLOCK RULE REVEALS — 2026-08-15.
   Each rule uses the same start delay and 900ms sweep as its text block. */
.promise-characters-ready .promise-points article {
  position: relative;
  border-top: 0;
}

.promise-characters-ready .promise-points article:before {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .9s cubic-bezier(.16, 1, .3, 1);
  transition-delay: var(--line-delay, 0ms);
}

.promise-characters-ready .promise-points article.is-line-visible:before {
  transform: scaleX(1);
}

/* ==========================================================================
   PORTFOLIO CHARACTER REVEALS — 2026-08-15.

   The section heading animates on entry; each card's browser label, title and
   call to action animate when that card reaches the viewport.
   ========================================================================== */

.work-characters-ready .work-word {
  display: inline-block;
  white-space: nowrap;
}

.work-characters-ready .work-char {
  display: inline-block;
  opacity: 0;
  filter: blur(5px);
  transform: translate3d(0, .72em, 0) rotateX(-42deg);
  transform-origin: 50% 100%;
  transition:
    opacity .48s cubic-bezier(.2, .7, .2, 1),
    transform .7s cubic-bezier(.16, 1, .3, 1),
    filter .55s cubic-bezier(.16, 1, .3, 1);
  transition-delay: var(--char-delay, 0ms);
}

.work-characters-ready .work-char.is-visible {
  opacity: 1;
  filter: blur(0);
  transform: translate3d(0, 0, 0) rotateX(0);
}

@media (prefers-reduced-motion: reduce) {
  .work-characters-ready .work-char {
    opacity: 1;
    filter: none;
    transform: none;
    transition: none;
  }
}

/* PORTFOLIO CARD TIMING TUNE — 2026-08-15.
   Card characters move more slowly; the section introduction is unchanged. */
.work-characters-ready .work-card .work-char {
  transition-duration: .62s, .95s, .78s;
}

/* ==========================================================================
   PROMISE MARQUEE REPOSITION — 2026-08-15.

   The promise content now leads the section. The ticker follows as a paced
   footer, with a responsive gap that keeps it visually separate from block 03.
   ========================================================================== */

.promise {
  padding-top: clamp(90px, 8vw, 120px);
  padding-bottom: 0;
}

@media (width <= 760px) {
  .promise {
    padding-top: 72px;
    padding-bottom: 0;
  }
}

/* ==========================================================================
   SITE-WIDE CHARACTER REVEALS — TRIAL, 2026-08-15.

   The current version 64 is checkpointed outside public_html. Promise and
   portfolio keep their bespoke sequences; this layer covers the remaining
   visible site copy without touching forms, logos or decorative text.
   ========================================================================== */

main.sitewide-text-ready .sitewide-text-run {
  display: inline;
}

main.sitewide-text-ready .sitewide-word {
  display: inline-block;
  white-space: nowrap;
}

main.sitewide-text-ready .sitewide-char {
  display: inline-block;
  opacity: 0;
  filter: blur(5px);
  transform: translate3d(0, .72em, 0) rotateX(-42deg);
  transform-origin: 50% 100%;
  transition:
    opacity .48s cubic-bezier(.2, .7, .2, 1),
    transform .7s cubic-bezier(.16, 1, .3, 1),
    filter .55s cubic-bezier(.16, 1, .3, 1);
  transition-delay: var(--char-delay, 0ms);
}

main.sitewide-text-ready .sitewide-char.is-visible {
  opacity: 1;
  filter: blur(0);
  transform: translate3d(0, 0, 0) rotateX(0);
}

@media (prefers-reduced-motion: reduce) {
  main.sitewide-text-ready .sitewide-char {
    opacity: 1;
    filter: none;
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   LIST ITEMS ARRIVE AS BLOCKS — 2026-08-15.

   Tick lists, editing steps and the process list are no longer split into
   characters: a bulleted line read letter by letter looks like noise rather
   than like writing. Each item moves as one piece, and the JavaScript walks
   them down the list on the shared beat (SEQUENCE_TIMING.item), so the
   stagger is between items instead of between letters.
   ========================================================================== */

main.sitewide-text-ready .sitewide-block {
  opacity: 0;
  transform: translate3d(0, 12px, 0);
  transition:
    opacity .42s cubic-bezier(.2, .7, .2, 1),
    transform .6s cubic-bezier(.16, 1, .3, 1);
  transition-delay: var(--block-delay, 0ms);
}

main.sitewide-text-ready .sitewide-block.is-visible {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

@media (prefers-reduced-motion: reduce) {
  main.sitewide-text-ready .sitewide-block {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   DESKTOP SEQUENCE SPEED — 2026-08-15.

   The staggered sections were paced for a phone, where each block arrives on
   its own. On a desktop they are all on screen at once and the wait between
   them read as slow, so the letters travel quicker and the rule under each
   promise block draws in roughly half the time. The sequence itself is timed
   in main.js (SEQUENCE_TIMING) — this only shortens the individual moves.
   ========================================================================== */

@media (min-width: 761px) and (prefers-reduced-motion: no-preference) {
  .promise-characters-ready .promise-char,
  main.sitewide-text-ready .sitewide-char {
    transition-duration: .34s, .5s, .4s;
  }

  .promise-characters-ready .promise-points article:before {
    transition-duration: .55s;
  }

  main.sitewide-text-ready .sitewide-block {
    transition-duration: .34s, .46s;
  }

  /* The three cards follow the same beat as the three promise blocks:
     SEQUENCE_TIMING.step, which is 450ms on a desktop. Below this width they
     stack and arrive one at a time, so they keep their small offsets. */
  .price-grid .price-card:nth-of-type(2) { --delay: 450ms; }
  .price-grid .price-card:nth-of-type(3) { --delay: 900ms; }
}

/* The offsets the cards carried inline before, kept for the stacked layout. */
@media (width <= 760px) {
  .price-grid .price-card:nth-of-type(2) { --delay: 90ms; }
  .price-grid .price-card:nth-of-type(3) { --delay: 180ms; }
}


/* ==========================================================================
   PROMISE GAP + PRICING BUTTON ALIGNMENT — 2026-08-15.

   The hidden ticker no longer supplies separation before Selected Work.
   Pricing CTAs use explicit label/plan/arrow grid positions so their layout
   does not depend on comment-separated anonymous flex items.
   ========================================================================== */

#promise {
  padding-bottom: clamp(72px, 9vw, 130px);
}

/* ==========================================================================
   PRICING BUTTONS — 2026-08-15.

   "Choose" and the plan name read as one phrase at the left, with the arrow
   pushed out to the right edge. They were previously a three-column grid
   which centred the plan name on its own, leaving the two halves of the same
   sentence sitting apart. position:relative keeps both labels above the
   button's tinted :before wash.
   ========================================================================== */

.pricing .price-card .button-card {
  display: flex;
  align-items: center;
  gap: .32em;
}

.pricing .price-card .button-card .button-choice,
.pricing .price-card .button-card .button-plan {
  position: relative;
}

.pricing .price-card .button-card .arrow {
  margin-left: auto;
}

/* The button arrives on the same beat as a tick item — one step after the
   last tick in its card — but it FADES rather than rising into place. A tick
   is a line of text and can travel; the button is a solid object the width of
   the card, and 12px of drop on something that size reads as a thump. So the
   rise is cancelled here and the whole move is carried by opacity.

   Its own transition has to be redeclared in full, because a single
   `transition` declaration wins outright and the button's base rule would
   otherwise drop the reveal's timing and per-card delay. The hover lift also
   moved off `transform` onto the independent `translate` property, so the two
   can never fight over the same value. */
main.sitewide-text-ready .pricing .price-card .button-card {
  transform: none;
  transition:
    opacity .4s cubic-bezier(.2, .7, .2, 1) var(--block-delay, 0ms),
    translate .18s ease,
    border-color .18s ease,
    background .18s ease,
    box-shadow .18s ease;
}

@media (min-width: 761px) and (prefers-reduced-motion: no-preference) {
  main.sitewide-text-ready .pricing .price-card .button-card {
    transition-duration: .32s, .18s, .18s, .18s, .18s;
  }
}

/* ==========================================================================
   FAQ SECTION — 2026-08-20.

   Plain <details>/<summary> — no JS needed to open/close, which fits the
   site's move toward fewer moving parts (see the pricing card note above).
   The whole list fades in as one [data-reveal] block, not item by item.
   ========================================================================== */

.faq {
  background: #fff;
}

.faq-list {
  max-width: 900px;
  margin: 64px auto 0;
  border-top: 1px solid var(--line);
}

.faq-item {
  border-bottom: 1px solid var(--line);
}

.faq-item summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 26px 4px;
  font-size: clamp(17px, 2vw, 20px);
  font-weight: 600;
  letter-spacing: -.01em;
}

.faq-item summary::-webkit-details-marker {
  display: none;
}

.faq-item .faq-icon {
  position: relative;
  flex: none;
  width: 22px;
  height: 22px;
}

.faq-item .faq-icon:before,
.faq-item .faq-icon:after {
  content: "";
  background: var(--ink);
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
}

.faq-item .faq-icon:before {
  width: 14px;
  height: 2px;
}

.faq-item .faq-icon:after {
  width: 2px;
  height: 14px;
  transition: transform .25s ease;
}

.faq-item[open] .faq-icon:after {
  transform: rotate(90deg);
}

.faq-item p {
  color: #5c605e;
  margin: 0 0 26px;
  padding-right: 46px;
  font-size: 16px;
  line-height: 1.65;
}

@media (width <= 760px) {
  .faq-list {
    margin-top: 44px;
  }

  .faq-item summary {
    padding: 20px 2px;
    font-size: 16px;
  }

  .faq-item p {
    padding-right: 0;
  }
}


/* ============================================================================
   SUBHEADING GRADIENT  —  added 2026-08-25
   ----------------------------------------------------------------------------
   Repaints the grey second line of each section heading (the "subheading") in
   the hero's shopfront colours, so the warm/cool palette carries on down the
   page instead of stopping at the hero. Black main headings, body copy and the
   grey section descriptions are all left exactly as they were.

   Colours are the hero's own shopfront samples, trimmed to the ones that stay
   legible on BOTH the light (#f7f7f3) and near-black (#0c0d0e) sections. The
   deep-shadow rust (#9A2E0A) and deep window-blue (#12649E) are dropped on
   purpose - they disappear on the dark blocks (Pricing, Contact). What's left
   is a warm sweep with a turquoise heart:

       #EF6A16  open-door orange
       #F69C11  amber window glow
       #EFC74F  warm light on the stone
       #29B4CF  turquoise of the right-hand window

   It's a palindrome at 200% width and drifts left-to-right on a 10s loop, the
   same technique and timing as the hero wordmark (.hero h1 em / hero-em-drift),
   so the two read as a family and the loop has no seam.

   WHY THESE SPANS ARE NEVER LETTER-SPLIT: a per-letter reveal puts each
   character on its own paint layer, and background-clip:text cannot paint
   one gradient across separate layers - the letters render invisible. Two
   attempts at neutralising this in CSS (forcing transform/opacity/filter to
   static values, then also forcing `transition:none`) still failed on real
   Safari and mobile, because the FAQ heading span ("Here's the short
   answer.") was the control that gave it away: it was never run through
   main.js's character-splitting at all, and it was the one subheading that
   never broke anywhere. So the fix moved from CSS to source: every one of
   these subheading spans carries class="heading-gradient" in index.php, and
   all three character-splitting routines in main.js skip any text node
   inside `.heading-gradient` - the text stays as one plain, unsplit run, so
   there is nothing for a browser to composite onto separate layers. The
   BLACK main headings above each of these spans keep their letter-by-letter
   fly-in untouched; only the gradient line itself is exempt.

   ALL EIGHT SECTION SUBHEADINGS CARRY THIS CLASS (the FAQ one, "Here's the
   short answer.", was missed in the original pass and added 2026-08-26 - it
   was falling back to the plain grey `h2 span` rule), not just the four on
   light backgrounds. Pricing, Easy Editing and Contact used to look plain
   grey, but that was never a deliberate light/dark split - it was a
   specificity accident (see the !important note on the rule below) that
   happened to hold up right until the character-splitting fix above
   started excluding elements by class instead of by an unscoped selector,
   at which point those three went fully blank instead. The colour palette
   was already chosen to stay legible on the near-black Pricing/Contact
   backgrounds (see above), which is what made restoring the gradient there
   the safe fix rather than a new design decision - flagged to the user
   rather than assumed.

   TO REVERSE ON ALL EIGHT: delete this whole block AND remove
   class="heading-gradient" from every <span> in index.php (search the
   file for it). The original grey rules earlier in this file take back
   over, untouched. To revert ONLY Pricing/Easy Editing/Contact back to
   grey: remove class="heading-gradient" from just those three spans (the
   ones inside "Pick a budget.", "Make a change." and "Let's make your").
   ========================================================================== */

@keyframes skyline-subhead-drift {
  from { background-position: 0% center; }
  to   { background-position: 200% center; }
}

.heading-gradient {
  /* DESCENDER CLIPPING — fixed 2026-09-04. Every span here inherited its
     parent heading's line-height, which sitewide is tuned tight (.86 to
     1.06) for a chunky display-type look. That is fine for plain black text,
     but this span also has an infinite animation on background-position
     (skyline-subhead-drift, below), and animating that property forces the
     browser to promote the gradient paint onto its own GPU compositing
     layer - sized to the LINE BOX, not to the full ink extent of the glyphs.
     Any descender that reaches below a line box shorter than the font's own
     descent (g, y, p, j, q) gets its tail sliced off flat by that layer,
     invisibly - overflow:visible does not help, because nothing is
     overflowing a CSS box in the normal sense; a compositing layer is
     clipping to its own bounds. Confirmed on "We've got answers." (the g in
     "got") and "a Signature build?" (the g in "Signature") - every other
     current subheading happens to avoid g/y/p/j/q entirely, which is why
     this sat unnoticed until those two.

     line-height:1.15 here overrides the inherited value for this element
     only - the plain black text on the line above (a separate line, always
     split from this by a manual <br/>) keeps its own tighter parent
     line-height untouched, so none of the site's tuned heading rhythm
     changes. 1.0 was the bare minimum that stopped the clip in this browser;
     1.15 leaves headroom for another engine's slightly different font
     metrics (this was first spotted on a phone, not here) without
     introducing a visible gap - verified against every current subheading.

     .hero h1 em ("Made visible") has the exact same shape of risk - gradient
     text, animated background-position, inherited line-height:.86 - but
     currently has no descender in its copy, so it is not visibly broken and
     was deliberately left alone rather than touching hero type pre-emptively.
     If that copy ever changes to include a g/y/p/j/q, it will need the same
     treatment. */
  line-height: 1.15;
  background-image: linear-gradient(
      90deg,
      #EF6A16 0%,
      #F69C11 14%,
      #EFC74F 28%,
      #29B4CF 50%,
      #EFC74F 72%,
      #F69C11 86%,
      #EF6A16 100%);
  background-size: 200% 100%;
  -webkit-background-clip: text;
          background-clip: text;
  /* !important: three older, more specific selectors (.admin-intro h2 span,
     .contact h2 span, .pricing .section-heading h2 span - all pre-dating
     this gradient feature) still set a plain grey `color` and would
     otherwise out-specify this single class selector on Pricing, Easy
     Editing and Contact. Those rules are dead now that all eight
     subheadings share this treatment; left in place only as a documented
     fallback in case the gradient is ever reversed. */
  -webkit-text-fill-color: transparent !important;
          color: transparent !important;
  animation: skyline-subhead-drift 10s linear infinite;
}

/* Reduced-motion: keep the colours, stop the drift. */
@media (prefers-reduced-motion: reduce) {
  .heading-gradient {
    animation: none;
    background-position: 50% center;
  }
}


/* ============================================================================
   HERO HEADLINE FILL  —  added 2026-08-25
   ----------------------------------------------------------------------------
   ".hero h1 em" ("Made visible") is meant to carry the animated shopfront
   gradient (see the .hero h1 em rule above). Its letters are split into
   per-character <sw-text-char> elements for the reveal, and each animated
   character sits on its own paint layer - which background-clip:text cannot
   fill across, so only the white -webkit-text-stroke showed and the letters
   read as hollow outlines.

   Holding those characters still lets the gradient fill the whole word, the
   same fix as the subheadings. "Your idea" (line one, solid white, outside
   the em) keeps its per-letter fly-in untouched; only the gradient line is
   held still.

   TO REVERSE: delete this block. "Made visible" returns to a hollow outline.
   ========================================================================== */

.hero h1 em
  :is(.sitewide-char, .sitewide-word, sw-text-char, sw-text-word, sw-text-run) {
  transform: none !important;
  opacity: 1 !important;
  filter: none !important;
  -webkit-text-fill-color: transparent !important;
}


/* ============================================================================
   "MADE TO STAND OUT." — FORCED SINGLE LINE  —  added 2026-08-25
   ----------------------------------------------------------------------------
   The #work h2's second line ("Made to stand out.") shares the site's
   gradient-text treatment (see "SUBHEADING GRADIENT" above), which paints one
   background-clip:text gradient across the whole line. When the line wraps to
   two rows the gradient smears - the drift only reads correctly on one row.

   The shared h2 rule sizes this text with the SAME clamp(42px,5.2vw,80px) as
   "Built to fit." above it, but the two lines sit in a flex row next to the
   section's intro paragraph, so the actual space available to the heading
   shrinks faster than the viewport-relative font does - it started wrapping
   anywhere from ~1100px down to ~760px, and again below ~430px on phones.

   Fix: give just this span its own, faster-shrinking clamp (a smaller vw
   coefficient, same 80px cap so wide screens are unchanged) plus
   white-space:nowrap as a backstop. Verified one line, no layout overflow,
   at every width from 320px to 1920px.
   ========================================================================== */

.work-heading h2 span:not(.work-word):not(.work-char) {
  white-space: nowrap;
  font-size: clamp(24px, 4.6vw, 80px);
}

/* ============================================================================
   SELECTED WORK — DIVIDERS BETWEEN CARDS  —  added 2026-08-26,
   turned dark later the same day
   ----------------------------------------------------------------------------
   The work grid is a single full-bleed column (see the ".work-grid" override
   above: grid-template-columns:1fr, gap:0, margin-inline:calc(50% - 50vw)), so
   the near-black .work-card tiles butt straight into one another with nothing
   marking where one ends and the next begins.

   These lines were originally 8px of pure white. That was chosen while .work
   was #e9eae5, an off-white: a white line belonged to the section around the
   cards. .work is now var(--ink) (see "WHOLE SECTION DARK" at the foot of
   this file) and white read as a hard stripe across a dark field, so the
   dividers now use --ink as well and match the heading area.

   Each card is a full-width screenshot in this layout (.work-browser is
   display:none, .work-meta is absolutely positioned over the image), so an
   --ink band still separates one tile from the next perfectly clearly — it
   is dark against photography, not dark against dark.

   Still border-top on every card AFTER the first rather than grid `gap`.
   With .work now the same colour, a gap would look identical; the border is
   kept because the adjacent-sibling selector keeps the rule honest about
   "between" — no line above the first card, none below the last. .work has
   padding-bottom:0, so a trailing border would read as a seam against the
   next section rather than as a divider.

   The border sits outside .work-card's padding box, so neither its
   overflow:hidden nor the inset:0 .work-card:after darkening overlay paints
   over it - the 8px stays a clean, even band.

   TO REVERSE: delete this block. For the original white, set the colour back
   to #fff (style.css.bak-2026-08-26-white-dividers is the file as it was).
   ========================================================================== */

.work-card + .work-card {
  border-top: 8px solid var(--ink);
}


/* ============================================================================
   FAQ HEADING — STACKED, LIKE EVERY OTHER SECTION  —  added 2026-08-26
   ----------------------------------------------------------------------------
   The FAQ was one of only two sections (pricing is the other) using the
   .section-heading three-column grid — .5fr / 1.25fr / .75fr with
   align-items:end, where column one holds the little SECTION LABEL and column
   two holds the h2. That grid is designed for a heading WITH an intro
   paragraph in column three; the FAQ has no third child, so the label ended up
   marooned in a column of its own, bottom-aligned against the side of the
   heading, and the whole block (max-width 1380px) started to the right of the
   .faq-list below it (max-width 900px). Two different left edges, and a label
   that read as floating rather than as a kicker.

   Every other section — .free-copy ("One good idea."), .research-copy,
   .admin-intro, .contact-intro — simply stacks: label, then h2 directly under
   it, both flush left. This makes the FAQ do the same by turning the grid off
   rather than by re-classing the markup, so the h2 keeps its shared
   `.section-heading h2` sizing (clamp(42px,5.2vw,80px) / -.055em / .98 / 700)
   with nothing duplicated and nothing to drift out of step later.

   Scoped to `.faq` so #pricing's .section-heading — which DOES have all three
   children and works as designed — is untouched.

   The 900px max-width lines the heading's left edge up with the accordion
   below it; 26px is the label-to-heading gap `.free-copy h2` uses, matched
   here so the two sections read identically.

   TO REVERSE: delete this block.
   ========================================================================== */

.faq .section-heading {
  display: block;
  max-width: 900px;
}

.faq .section-heading h2 {
  margin-top: 26px;
}


/* ============================================================================
   PROMISE INTRO PARAGRAPH  —  added 2026-08-26
   ----------------------------------------------------------------------------
   New copy sitting between the #promise h2 and the three numbered points.
   The section had no intro paragraph before, so there was nothing to inherit.

   Sized between the two things it sits between: larger than the 15px point
   copy under it, smaller than the h2 above it. max-width is a measure limit,
   not a layout one — the .section-grid column it lives in is much wider than
   comfortable reading length at desktop sizes.

   .promise-points already carries margin-top:66px, which stays as the gap
   below; only the gap above needs declaring here.
   ========================================================================== */

.promise-intro {
  color: #5d615f;
  max-width: 660px;
  margin: 30px 0 0;
  font-size: 17px;
  line-height: 1.7;
}

@media (width <= 760px) {
  .promise-intro {
    font-size: 16px;
    margin-top: 24px;
  }
}


/* ============================================================================
   CONTACT DETAILS — PHONE + ADDRESS  —  added 2026-08-26
   ----------------------------------------------------------------------------
   The site had no name/address/phone anywhere until now; contact was
   form-only, which was the largest single local-search handicap on it (see the
   SEO audit). Two placements, doing two different jobs:

   .contact-call sits directly under the #contact intro copy, next to the form,
   because that is where someone decides how to get in touch and some people
   will always rather talk than type. The number is the emphasis, not the
   label.

   .footer-nap is the conventional home for the full set, and the pattern
   search engines expect to find it in. <address> is the correct element for
   the contact details of the page it appears on - it is not only for postal
   addresses, and it carries no italics of its own here.

   Both are plain text and a tel: link, so the number is selectable, callable
   on a phone, and readable by anything crawling the page. It matches the
   `telephone` in the ProfessionalService schema exactly.
   ========================================================================== */

.contact-call {
  margin: 18px 0 0;
  font-size: 15px;
  color: #9aa0a0;
}

.contact-call a {
  color: #fff;
  font-weight: 700;
  font-size: 19px;
  letter-spacing: -.01em;
  text-decoration: none;
  border-bottom: 1px solid #4a4d4b;
  padding-bottom: 2px;
  margin-left: 6px;
  transition: border-color .25s;
}

.contact-call a:hover { border-bottom-color: #fff; }

.footer-nap {
  font-style: normal;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12px;
  color: #9fa3a0;
}

.footer-nap a {
  color: #fff;
  font-weight: 700;
  text-decoration: none;
  border-bottom: 1px solid #4a4d4b;
  padding-bottom: 1px;
  width: max-content;
}

.footer-nap a:hover { border-bottom-color: #fff; }

/* The footer is a three-column grid whose second column already holds the
   copyright line; the address takes the first column under the logo, so the
   logo and the details read as one block. */
footer .footer-nap { grid-column: 2; }
footer > p { grid-column: 2; }

@media (width <= 760px) {
  .contact-call a { font-size: 17px; }
  footer .footer-nap, footer > p { grid-column: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .contact-call a, .footer-nap a { transition: none; }
}


/* ============================================================================
   SERVICE PAGES  —  /launch, /elevate, /signature  —  added 2026-08-26
   ----------------------------------------------------------------------------
   Everything structural on these pages is already in this file: .site-header,
   footer, .button, .section-label, .heading-gradient, .faq-list/.faq-item and
   .section-pad are the home page's and are reused as-is, so a fix made there
   reaches these pages automatically. Only what is genuinely new lives here,
   and it is all prefixed .tier- so it cannot collide with anything above.

   The pages are light on the site's --paper ground with a dark closing CTA,
   which is the same light-to-dark rhythm the home page ends on.

   .site-header carries .is-stuck from the markup rather than waiting for the
   scroll handler to add it: on the home page the header starts transparent
   over a full-bleed night photograph, but these pages open on a light
   background, so an unstyled header would be white type on near-white.
   ========================================================================== */

.tier-page { display: block; }

.tier-page section { max-width: 1180px; margin-inline: auto; }

/* ---- hero ---------------------------------------------------------------- */

.tier-hero { padding-top: 148px; display: flex; flex-direction: column; align-items: flex-start; }

.tier-hero h1 {
  letter-spacing: -.055em;
  font-size: clamp(42px, 6.4vw, 88px);
  line-height: .92;
  font-weight: 700;
  margin: 18px 0 0;
  max-width: 15ch;
}

.tier-intro {
  margin: 30px 0 0;
  max-width: 62ch;
  font-size: clamp(17px, 1.5vw, 20px);
  line-height: 1.62;
  color: #4a4f4c;
}

.tier-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 20px; margin-top: 38px; }

/* ---- who it is for ------------------------------------------------------- */

.tier-fit {
  display: grid;
  grid-template-columns: 1.15fr .85fr;
  gap: clamp(30px, 5vw, 76px);
  align-items: start;
  border-top: 1px solid var(--line, #d9dcd7);
}

.tier-fit h2, .tier-includes h2, .tier-compare h2, .tier-faq h2 {
  letter-spacing: -.035em;
  font-size: clamp(26px, 3.1vw, 40px);
  line-height: 1.06;
  font-weight: 700;
  margin: 0 0 26px;
  max-width: 20ch;
}

.tier-fit ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.tier-fit li {
  position: relative;
  padding: 15px 0 15px 32px;
  border-top: 1px solid #dcdfda;
  font-size: 17px;
  line-height: 1.55;
  color: #3f4441;
}
.tier-fit li:last-child { border-bottom: 1px solid #dcdfda; }
.tier-fit li::before {
  content: "✓";
  position: absolute; left: 0; top: 15px;
  color: var(--coral, #EF6A16); font-weight: 900; font-size: 14px;
}

.tier-fit-no { background: #ecede8; padding: clamp(22px, 3vw, 34px); }
.tier-fit-no h2 { font-size: clamp(20px, 2vw, 25px); margin-bottom: 14px; }
.tier-fit-no p { margin: 0; font-size: 16.5px; line-height: 1.66; color: #4a4f4c; }
.tier-fit-no a { color: var(--ink); font-weight: 700; text-underline-offset: 3px; }

/* ---- what's included ----------------------------------------------------- */

.tier-includes dl { margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0 clamp(30px, 4vw, 64px); }
.tier-includes dl > div { padding: 24px 0; border-top: 1px solid #dcdfda; }
.tier-includes dt {
  font-weight: 700; font-size: 18.5px; letter-spacing: -.015em;
  margin-bottom: 9px; color: var(--ink);
}
.tier-includes dd { margin: 0; font-size: 16px; line-height: 1.68; color: #5c605e; }

/* ---- live example ---------------------------------------------------------
   Moved here 2026-09-03 - was a sub-block inside .tier-includes, now its own
   section between "who it's for" and "what you get" (see tier-template.php).
   Full rules, including the two responsive collapses, live together with the
   rest of the tier-page CSS further down this file under TIER PAGE — LIVE
   EXAMPLE; this comment marks where in page order it now sits. */

/* ---- compare ------------------------------------------------------------- */

.tier-compare { border-top: 1px solid #dcdfda; }
.tier-compare-intro { margin: -12px 0 34px; max-width: 60ch; font-size: 17px; line-height: 1.6; color: #5c605e; }
.tier-compare-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }

.tier-card {
  display: flex; flex-direction: column; gap: 8px;
  background: #fff; border: 1px solid #dcdfda;
  padding: clamp(22px, 2.6vw, 32px);
  text-decoration: none; color: var(--ink);
  transition: transform .3s, box-shadow .3s;
}
.tier-card:hover { transform: translateY(-4px); box-shadow: 0 18px 50px rgba(30, 35, 32, .1); }
.tier-card-name { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -.02em; }
.tier-card-price { margin: 0; font-size: 14px; color: #8a8d8b; font-weight: 700; }
.tier-card-line { margin: 6px 0 0; font-size: 15.5px; line-height: 1.6; color: #5c605e; }
.tier-card-go { margin-top: auto; padding-top: 20px; font-size: 13px; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; }
.tier-card--all { background: #ecede8; }

/* ---- faq + closing cta --------------------------------------------------- */

.tier-faq { border-top: 1px solid #dcdfda; }
.tier-faq .faq-list { max-width: none; margin-top: 0; }

.tier-cta {
  background: var(--ink); color: #fff;
  max-width: none !important;
  display: flex; flex-direction: column; align-items: flex-start;
}
.tier-cta h2 {
  letter-spacing: -.055em; font-size: clamp(34px, 5vw, 66px); line-height: .96;
  font-weight: 700; margin: 20px 0 0;
}
.tier-cta > p { margin: 26px 0 0; max-width: 56ch; font-size: 17px; line-height: 1.62; color: #bcc0bd; }
.tier-cta .text-link { color: #fff; }

@media (width <= 900px) {
  .tier-fit { grid-template-columns: 1fr; }
  .tier-includes dl { grid-template-columns: 1fr; }
  .tier-compare-grid { grid-template-columns: 1fr; }
}

@media (width <= 760px) {
  .tier-hero { padding-top: 118px; }
  .tier-actions { flex-direction: column; align-items: flex-start; gap: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .tier-card { transition: none; }
  .tier-card:hover { transform: none; }
}


/* ============================================================================
   HEADER ON THE SERVICE PAGES  —  2026-08-26
   ----------------------------------------------------------------------------
   The header is built for the home page, where it starts as white type sitting
   over a full-bleed night photograph and only turns dark once main.js adds
   .is-stuck past the hero. The service pages open on --paper, so that default
   rendered the logo and every nav link in #f7f7f3 on an #f7f7f3 ground - a
   completely invisible navigation bar.

   The first attempt at this put .is-stuck straight into the markup. That does
   not work and is worth recording: main.js runs
   `header.classList.toggle('is-stuck', y > stickPoint)` on load, so at scroll
   position 0 it takes the class straight back off again. State the JS owns
   cannot be pre-set from HTML - it has to be handled in CSS instead.

   So: on .tier-page the bar is simply always in its resolved state - dark type
   on the frosted panel - and .is-stuck then only changes its height, which is
   exactly what it does on the home page once you are past the hero.
   ========================================================================== */

/* SPECIFICITY MATTERS HERE, and the first attempt got it wrong. The rule
   being overridden is

     .site-header:not(.is-stuck):not(:has(nav.nav-open)) { color: var(--paper) }

   which scores 0-3-1 (:has() takes the specificity of its argument). A plain
   `.tier-page .site-header` is 0-2-0 and silently loses - the rule is in the
   stylesheet, the browser has it, and the header still renders in --paper.
   Repeating the same :not() chain underneath .tier-page takes it to 0-4-1.
   Every declaration in that block needs the same treatment, not just colour:
   the nav underline, the CTA pill and the mobile toggle bars are all painted
   --paper by sibling rules at the same specificity. */
.tier-page .site-header:not(.is-stuck):not(:has(nav.nav-open)) {
  color: var(--ink);
}

.tier-page .site-header:not(.is-stuck):not(:has(nav.nav-open)) nav > a:not(.nav-cta):after {
  background: var(--ink);
}

.tier-page .site-header:not(.is-stuck):not(:has(nav.nav-open)) .nav-cta {
  color: #fff;
  background: var(--ink);
}

.tier-page .site-header:not(.is-stuck):not(:has(nav.nav-open)) .menu-toggle span {
  background: var(--ink);
}

/* The frosted panel is on the :before, not the header (see the note at
   .site-header:before for why). Showing it from the first frame gives the
   dark type something to sit on while the page is still at the top. */
.tier-page .site-header:before { opacity: 1; }


/* The pricing card's secondary link through to its own service page. Sits
   under the "Choose" button - still quieter than it (the button is the
   conversion path; this is for the person who wants proof before committing)
   but no longer muted grey: it takes on the card's own tone (--tone-a, the
   same variable the border, the top glow and the checkmarks already use), so
   each card's "See a live example" reads as that card's colour rather than
   generic UI grey.

   MARGIN-LEFT:23px — the "Choose" button's text sits 23px in from the card's
   padding edge (22px of the button's own padding, plus its 1px border); this
   link has neither, so without the offset its text started 23px to the left
   of the button's, out of step with it. 23px reproduces that inset exactly
   so the two lines of text share a left edge. Scoped inside .pricing, which
   is the dark night theme, so --tone-a resolves correctly here. */
.price-more {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 14px;
  margin-left: 23px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--tone-a);
  text-decoration: none;
  transition: color .25s;
}

.price-more:hover { color: #fff; }
.price-more .arrow { font-size: 13px; }

@media (prefers-reduced-motion: reduce) { .price-more { transition: none; } }
/* ==========================================================================
   SELECTED WORK — WHOLE SECTION DARK, 2026-08-26

   #work was #e9eae5, an off-white, while .work-grid inside it had already
   been made full-bleed with cards on background:var(--ink). That left a
   pale header band sitting on top of an edge-to-edge dark strip. The whole
   section is now --ink, so the heading reads as part of the same dark
   field as the cards rather than as a lighter lid on top of them.

   The heading itself gets NO background, radius or padding of its own —
   it is not a card. It keeps its original geometry (the base rule's
   `margin:auto 68px` and the flex row) and only changes colour, following
   the conventions of the site's other dark sections (see .contact):
   #fff headline, #afb2b0 label, #b9bdba body copy. The .heading-gradient
   span needs no override; it is already used on Pricing, Easy Editing and
   Contact, all dark.

   NOTE ON ADJACENCY: #work now runs straight into #pricing (#0c0d0e), two
   dark sections in a row — the thing the "EASY EDITING — DARK AGAIN" note
   further up this file was written about. Done deliberately at request.
   The two are not the same black (--ink #101112 vs #0c0d0e), so the seam
   is visible but soft; if it reads as one slab, giving #work a hairline
   bottom border or setting them to the same value are the two knobs.

   NOTE ON THE FIRST CARD: the white 8px dividers are `.work-card +
   .work-card`, so by design there is no line above the FIRST card. That
   boundary used to be marked by the colour change from #e9eae5 to the
   card's --ink; with the section now --ink too, nothing marks it, and the
   heading area runs seamlessly into the first tile. That is the intended
   effect here, but it is the one thing this change gives up.

   TO REVERSE: delete this block. The backup alongside style.css
   (style.css.bak-2026-08-26-work-heading-dark) is the file before any of
   this, back when the heading was dark text on off-white.
   ========================================================================== */

.work {
  background: var(--ink);
  color: #fff;
}

.work-heading .section-label {
  color: #afb2b0;
}

.work-heading h2 {
  color: #fff;
}

/* Was 15px / #5d615f / 440px. */
.work-heading > p {
  color: #b9bdba;
  font-size: 17.5px;
  line-height: 1.7;
  max-width: 470px;
}

@media (width<=760px) {
  .work-heading > p {
    font-size: 16.5px;
  }
}

/* ============================================================================
   WORK HEADING — AMBIENT GLOW  —  added 2026-09-04, moved and rebalanced
   2026-09-04
   ----------------------------------------------------------------------------
   Two soft radial blobs behind the "Selected work" heading, the same
   technique as the two glows on each .pricing .price-card (position:
   absolute, z-index:-1, radial-gradient faded to transparent, heavy blur,
   low opacity - a light source, not a shape). Neither section clips its
   glows with overflow:hidden - the pricing cards don't either - a large
   blur and a low opacity read as ambient light without needing a hard edge,
   so the pixels that spill past the heading's own box just fade into the
   section's flat --ink background.

   COLOUR CHOICE: not the pricing cards' cyan/amber/magenta trio, which was
   deliberately kept local to .pricing ("kept local so none of it leaks out
   of the section" - see the --pc-* declaration above). Instead, the two
   outermost stops of the .heading-gradient sweep already painted across
   "Made to stand out." right above these glows: #EF6A16 (its first stop)
   and #29B4CF (its middle stop) - so the light reads as coming from the
   same gradient the heading is already carrying, not a colour borrowed
   from elsewhere on the page.

   POSITION — both moved to the centre-right of the heading row, at the
   user's request: originally one sat in the top-left corner behind
   "Selected work" and the other off the right edge behind the paragraph.
   Now both cluster together, left:58%/68% and vertically centred on the
   row with top:50%/translateY(-50%) rather than a fixed pixel offset, so
   the cluster stays centred on the row's own height however tall the
   heading text runs. That puts them roughly where the paragraph begins,
   reading as one warm-to-cool light source rather than two separate
   corner accents.

   STRENGTH — the turquoise (#29B4CF) is now the dominant of the two, also
   at the user's request: width/height 300px -> 460px and opacity .18 -> .32
   for real reach, blur eased up too (24px -> 30px) so the extra size stays
   soft rather than gaining a harder edge. The orange sits just behind and
   left of it, unchanged in strength, so it reads as a secondary warm accent
   inside the turquoise's glow rather than a rival focal point.

   isolation:isolate matches the pricing card again - keeps the z-index:-1
   pseudo-elements contained to .work-heading's own stacking context so
   they can't be pulled behind an ancestor's content by accident.

   TO REVERSE: delete this block.
   ========================================================================== */

.work-heading {
  position: relative;
  isolation: isolate;
}

.work-heading:before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 50%;
  left: 58%;
  width: 300px;
  height: 300px;
  opacity: .2;
  border-radius: 50%;
  background: radial-gradient(circle, #EF6A16, transparent 68%);
  filter: blur(24px);
  transform: translateY(-50%);
}

.work-heading:after {
  content: "";
  position: absolute;
  z-index: -1;
  top: 50%;
  left: 68%;
  width: 460px;
  height: 460px;
  opacity: .32;
  border-radius: 50%;
  background: radial-gradient(circle, #29B4CF, transparent 70%);
  filter: blur(30px);
  transform: translateY(-50%);
}

@media (max-width: 760px) {
  /* These are static, not animated - nothing here is about motion. Below
     760px .work-heading switches from the flex row to a stacked column
     (see .work-heading{display:block} above), so "centre-right of the row"
     no longer means anything - the glows are dropped rather than
     repositioned for a layout this different. */
  .work-heading:before,
  .work-heading:after {
    display: none;
  }
}

/* ============================================================================
   SELECTED WORK — HEADING SITS HIGHER  —  added 2026-08-26
   ----------------------------------------------------------------------------
   The heading was taking .section-pad's 120px top padding (82px on mobile)
   and then only clamp(52px,5vw,92px) of clearance beneath it, so it sat low
   in its own space and crowded the first portfolio tile.

   Both halves move: less room above, more below. The section keeps
   padding-bottom:0 (set further up this file) so the last card still runs
   flush into #pricing.

   Scoped to .work rather than changed on .section-pad, which every other
   section on the page shares.

   TO REVERSE: delete this block.
   ========================================================================== */

.work {
  padding-top: clamp(58px, 5.4vw, 76px);
}

.work-heading {
  margin-bottom: clamp(78px, 7.5vw, 128px);
}

@media (width<=760px) {
  .work {
    padding-top: 46px;
  }

  .work-heading {
    margin-bottom: 60px;
  }
}

/* ============================================================================
   PORTFOLIO CARD TAGS  —  added 2026-08-26
   ----------------------------------------------------------------------------
   Three hooks under each card title, saying what the site actually is before
   the visitor commits to a click. Separated with the same middot the hero
   eyebrow uses ("Wirral roots · worldwide reach · 25+ years' experience"),
   so the two read as one house device rather than two list styles.

   No layout of its own: .work-meta>span:first-child is already a flex column
   with gap clamp(12px,1.4vw,20px), so dropping the span between <b> and
   .work-cta puts it in the right place with the right spacing. Sitting on a
   screenshot, so the same #fff-at-reduced-opacity treatment as .work-cta.

   CHARACTER ANIMATION: .work-tags is passed to splitText() in main.js
   (search "card.querySelector('.work-tags')"), between the title and the
   call to action, so the sweep runs title -> tags -> CTA in reading order.
   Nothing is needed here for that - the .work-char rules further up this
   file are generic and pick these up like any other split text.

   TO REVERSE: delete this block, the six spans in index.php (search
   "work-tags") and the one line in main.js.
   ========================================================================== */

.work-tags {
  color: #fff;
  font-size: clamp(12px, 1.05vw, 16px);
  font-weight: 500;
  letter-spacing: .045em;
  line-height: 1.45;
  opacity: .68;
  max-width: 34ch;
}

@media (max-width: 760px) {
  .work-tags {
    letter-spacing: .03em;
    max-width: 26ch;
  }
}

/* ============================================================================
   PORTFOLIO CTA — SOLID ARROW  —  added 2026-08-26
   ----------------------------------------------------------------------------
   "See the website" was a small underlined line of text and read as a
   footnote rather than an invitation. It is now "See it live" behind a solid
   white disc with a filled arrow in it, and the underline is gone - the disc
   is the affordance now, so an underline as well would be two of them.

   The arrow is drawn with clip-path on a ::before rather than set as a glyph.
   Characters like ➜ and ➡ fall through to whatever symbol or emoji font the
   device has, which on some phones comes back coloured and differently sized;
   a clipped box is the same shape everywhere and takes currentColor.

   ANIMATING IN STEP WITH THE LETTERS: the disc holds no text, so splitText()
   in main.js has nothing to split and it would otherwise sit there fully
   drawn while the label assembled letter by letter beside it. main.js now
   adds `is-revealed` to each card as its sweep starts (search "is-revealed"),
   which is what fades the disc in, 420ms behind the first character.

   Everything here is scoped under .work-characters-ready, the class main.js
   only adds when it is actually going to animate. Under
   prefers-reduced-motion the whole reveal module returns early, so that class
   never appears and the disc is simply visible from the start.

   TO REVERSE: delete this block, and in index.php swap the nine
   .work-cta spans back to plain "See the website" text.
   ========================================================================== */

.work-cta {
  display: inline-flex;
  align-items: center;
  gap: clamp(9px, .7vw, 12px);
  text-decoration: none;
  font-weight: 600;
  opacity: 1;
}

.work-cta-arrow {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: clamp(30px, 2.5vw, 40px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: #fff;
  transition:
    transform .38s cubic-bezier(.16, 1, .3, 1),
    opacity .5s ease;
}

.work-cta-arrow::before {
  content: "";
  width: 44%;
  height: 44%;
  background: var(--ink);
  clip-path: polygon(0 38%, 60% 38%, 60% 10%, 100% 50%, 60% 90%, 60% 62%, 0 62%);
}

.work-card:hover .work-cta-arrow {
  transform: translateX(5px);
}

/* Hidden only while the letter sweep is armed, then brought in behind it. */
.work-characters-ready .work-card .work-cta-arrow {
  opacity: 0;
  transform: translateX(-8px);
}

.work-characters-ready .work-card.is-revealed .work-cta-arrow {
  opacity: 1;
  transform: none;
  transition-delay: .42s;
}

.work-characters-ready .work-card.is-revealed:hover .work-cta-arrow {
  transform: translateX(5px);
  transition-delay: 0s;
}

@media (prefers-reduced-motion: reduce) {
  .work-cta-arrow,
  .work-characters-ready .work-card .work-cta-arrow {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ============================================================================
   PORTFOLIO CARD TEXT — SCALES ON PHONES  —  added 2026-08-27
   ----------------------------------------------------------------------------
   All four of these were clamp(min, Nvw, max) with a vw coefficient tuned for
   desktop, so on anything narrower than roughly 600px the vw term fell under
   the minimum and every one of them pinned flat to its floor: 30px title,
   12px tags, 13px CTA, 30px disc, identical on a 320px phone and a 600px
   tablet. Small, and not actually responsive down there at all.

   Replaced with clamp(min, calc(Nvw + Cpx), max). The constant does the work
   a bare vw could not: it lifts the whole curve so the phone end starts at a
   readable size, while the smaller coefficient keeps the slope gentle enough
   that the desktop cap lands where it always did. Every value below is
   unchanged at 1440px and above - this is a change to the phone and tablet
   half of the range only.

   Deliberately NOT done as a max-width media query. These sit on top of a
   photograph with no reflow to hide a step, so a jump at the breakpoint would
   be visible; a continuous curve has no breakpoint to jump at.

   Roughly, title / tags / CTA / disc:
     320px   36.2 / 15   / 15   / 24
     375px   38.0 / 15   / 15   / 24
     430px   39.8 / 15   / 15.4 / 24.6
     768px   50.6 / 16   / 16   / 27.8
     1440px  72   / 16   / 16   / 28      (text as before; the disc was
                                              deliberately shrunk 2026-08-27)

   TO REVERSE: delete this block. The originals are the clamp() values named
   in the first paragraph above.
   ========================================================================== */

.work-meta b {
  font-size: clamp(34px, calc(3.2vw + 26px), 72px);
}

.work-tags {
  font-size: clamp(15px, calc(0.6vw + 12.3px), 16px);
}

.work-cta {
  font-size: clamp(15px, calc(0.7vw + 12.4px), 16px);
}

.work-cta-arrow {
  width: clamp(24px, calc(0.95vw + 20.5px), 28px);
}

/* ============================================================================
   SECTION SUBHEADINGS — CONSISTENT SIZE ON PHONES  —  added 2026-08-27
   ----------------------------------------------------------------------------
   Every section h2 on the home page shares one size, clamp(42px,5.2vw,80px),
   and its gradient subheading span simply inherits it - so heading and
   subheading are the same size in Promise, Pricing, Easy editing, Free
   website, FAQ and Contact. #work was the single exception: "Made to stand
   out." carried its own clamp(24px,4.6vw,80px) plus white-space:nowrap (see
   "MADE TO STAND OUT. — FORCED SINGLE LINE" above), which on a 375px phone
   rendered it at 24px under a 42px heading. A 0.57 ratio where every other
   section was 1.0, which is what made the set look unstable.

   That rule was there to stop the line wrapping, because a wrapped
   background-clip:text gradient smears: with the default box-decoration-break
   the fragments are painted as though joined end to end, so the colour jumps
   mid-phrase.

   Both halves are addressed:

   1. .heading-gradient becomes inline-block. The gradient then paints across
      ONE box instead of per line fragment, so it stays continuous when the
      text wraps - each row shows the full left-to-right sweep. Applied at
      every width, but it changes nothing above the phone range because these
      lines do not wrap there. This also quietly fixes the other subheadings,
      several of which ("We've got answers.", "We'll do the rest.") were
      already wrapping and smearing on narrow phones.

   2. Below 760px the #work exception is stood down - font-size:inherit and
      white-space:normal - so it matches its own h2 like every other section.
      It wraps to two lines on a phone, which is what the rest of them
      already do.

   The desktop rule is untouched. Above 760px .work-heading is still a flex
   row sharing width with the intro paragraph, which is the squeeze the
   original nowrap was written for.

   TO REVERSE: delete this block.
   ========================================================================== */

.heading-gradient {
  display: inline-block;
  max-width: 100%;
}

@media (max-width: 760px) {
  .work-heading h2 span:not(.work-word):not(.work-char) {
    font-size: inherit;
    white-space: normal;
  }
}

/* ============================================================================
   PORTFOLIO CTA — ARROW ONLY  —  added 2026-08-27
   ----------------------------------------------------------------------------
   "See it live" is no longer drawn; the disc carries the invitation on its
   own. The words are kept in the markup and hidden visually rather than
   deleted, for two reasons:

   1. The disc is aria-hidden (it is a clipped box, not a glyph, so it has no
      text to read). With the label deleted outright the only thing left in
      the card would be the business name and the three tags, and nothing
      would say the link opens the live site - which matters more here than
      usual because every one of these carries target="_blank".

   2. splitText() in main.js reads .work-cta's innerText to set its
      aria-label. An empty element gets no label at all.

   The card as a whole is the link, so the small disc is not the tap target -
   the whole tile is.

   TO REVERSE: drop .work-cta-label to `display:inline` and the words come
   back, no markup change needed.
   ========================================================================== */

.work-cta-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  white-space: nowrap;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
}

/* ============================================================================
   PORTFOLIO CARDS — LEFT-HAND SCRIM  —  added 2026-08-27
   ----------------------------------------------------------------------------
   .work-card:after already carried a radial wash anchored at 0% 100%, so its
   darkest point was the BOTTOM-left corner. .work-meta is centred vertically
   (align-items:center) against the LEFT edge, so the title, tags and disc sat
   above the strongest part of the wash rather than in it, and how readable
   they were came down to whatever the screenshot happened to be doing behind
   them - fine over Del Mar's dark interior, weak over King Coffee's pale one.

   A horizontal scrim is layered over the top of the original. Listed first in
   the shorthand, so it paints above the radial; the radial is repeated here
   verbatim because `background` is a shorthand and would otherwise drop it.

   Deliberately a flat left-to-right ramp rather than a stronger radial: the
   text column runs the full height of the card on the left, so darkening by
   x alone covers it evenly, while pulling the radial up would have muddied
   the middle of the photograph as well.

   Clears to nothing by 88% so the right-hand side of every screenshot - the
   part that is actually the portfolio piece - is untouched.

   On phones the ramp is a little stronger at the edge but falls away
   FASTER than the desktop one (clear by 84%, against 88%), so more of the
   screenshot stays visible on a small screen where there is least of it
   to show. The extra weight at 0% is there to hold contrast under the
   text despite the shorter run.

   TO REVERSE: delete this block; the original radial-only rule near
   ".work-card:after" further up the file takes back over.
   ========================================================================== */

.work-card:after {
  background:
    linear-gradient(90deg,
      rgba(16, 17, 18, .60) 0%,
      rgba(16, 17, 18, .46) 30%,
      rgba(16, 17, 18, .22) 55%,
      rgba(16, 17, 18, .05) 75%,
      rgba(16, 17, 18, 0) 88%),
    radial-gradient(140% 120% at 0% 100%,
      rgba(16, 17, 18, .85) 0%,
      rgba(16, 17, 18, .60) 30%,
      rgba(16, 17, 18, .35) 55%,
      rgba(16, 17, 18, .12) 72%,
      rgba(16, 17, 18, 0) 88%);
}

@media (max-width: 760px) {
  .work-card:after {
    background:
      linear-gradient(90deg,
        rgba(16, 17, 18, .68) 0%,
        rgba(16, 17, 18, .54) 26%,
        rgba(16, 17, 18, .30) 50%,
        rgba(16, 17, 18, .10) 70%,
        rgba(16, 17, 18, 0) 84%),
      radial-gradient(140% 120% at 0% 100%,
        rgba(16, 17, 18, .85) 0%,
        rgba(16, 17, 18, .60) 30%,
        rgba(16, 17, 18, .35) 55%,
        rgba(16, 17, 18, .12) 72%,
        rgba(16, 17, 18, 0) 88%);
  }
}

/* ============================================================================
   CONTACT FORM — PREFILLED LEVEL  —  added 2026-08-27
   ----------------------------------------------------------------------------
   When main.js sets "Which level feels right?" from a pricing card or a tier
   page (search "PRICING -> CONTACT FORM PRESELECT"), the field flashes once so
   the visitor registers that a choice has already been made on their behalf
   and can change it - a select that silently arrives non-empty is easy to
   scroll straight past.

   Borrows the existing focus treatment (--ink border, cyan ring) rather than
   inventing a third state, so it reads as "this field is active" in the
   vocabulary the form already uses.

   Runs once and stops. It is an acknowledgement, not an attention-getter.

   TO REVERSE: delete this block. The prefill still works; it just happens
   without visible comment.
   ========================================================================== */

@keyframes skyline-prefill {
  0%   { border-color: #cbd0cc; box-shadow: 0 0 0 0 #13cbd700; }
  18%  { border-color: var(--ink); box-shadow: 0 0 0 4px #13cbd747; }
  100% { border-color: #cbd0cc; box-shadow: 0 0 0 0 #13cbd700; }
}

.contact-form select.is-prefilled {
  animation: skyline-prefill 1.5s ease-out 1;
}

@media (prefers-reduced-motion: reduce) {
  .contact-form select.is-prefilled {
    animation: none;
  }
}

/* ============================================================================
   FORM CONFIRMATIONS — FULL PANEL  —  added 2026-08-27
   ----------------------------------------------------------------------------
   Both forms used to keep rendering after a successful post, with a small
   green notice bar above them (.form-success / .dark-success). Two problems
   with that. It read as a validation message rather than as good news, and
   the form itself carries data-reveal, so on the way back down to #contact the
   letters and fields were still animating in - a visitor arriving at their own
   confirmation could find the panel empty for a beat, which is what it looked
   like in practice.

   The form is now REPLACED by this panel on success (see the if/else in
   index.php), so there is no half-revealed form underneath and no second
   copy of a form the visitor has already completed. No data-reveal on the
   panel: a confirmation must not need a scroll to appear.

   Sized below the section h2 rather than matching it, so it is clearly large
   and celebratory without competing with "Let's make your business visible."
   next to it. Uses .heading-gradient, so the good-news line carries the same
   colour sweep as every subheading on the site.

   .form-done-light is the #free-website variant, on white rather than --ink.

   The old .form-success / .dark-success rules are left in place, unused, in
   case the small-notice pattern is wanted somewhere else.

   TO REVERSE: delete this block and put the two if/else blocks in index.php
   back to `if ... <p class="form-success">... endif` above the form.
   ========================================================================== */

.form-done {
  align-self: start;
  max-width: 30ch;
}

.form-done .section-label {
  color: #9aa0a0;
  margin: 0 0 22px;
}

.form-done-head {
  margin: 0;
  font-size: clamp(34px, 4vw, 58px);
  font-weight: 700;
  letter-spacing: -.05em;
  line-height: .99;
  color: #fff;
}

.form-done-copy {
  margin: 26px 0 0;
  max-width: 40ch;
  color: #b9bdba;
  font-size: 16px;
  line-height: 1.7;
}

.form-done-more {
  margin: 20px 0 0;
  font-size: 15px;
  color: #b9bdba;
}

.form-done-more a {
  color: #fff;
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 5px;
}

/* #free-website sits on white, so the panel flips to the page's ink text. */
.form-done-light .form-done-head {
  color: var(--ink);
}

.form-done-light .section-label,
.form-done-light .form-done-copy,
.form-done-light .form-done-more {
  color: #5f6361;
}

.form-done-light .form-done-more a {
  color: var(--ink);
}

/* ----------------------------------------------------------------------------
   The redirect after a post lands on /?sent=...#contact. html has
   scroll-behavior:smooth, so the browser was ANIMATING the whole way from the
   top of the page down to the section - the "jump scrolling" on the way back
   from a send. This class is only present on that return, and turns the
   fragment jump back into an instant one, so the visitor arrives already
   looking at their confirmation.
   -------------------------------------------------------------------------- */

html.form-return {
  scroll-behavior: auto;
}

/* ============================================================================
   CONTACT CONFIRMATION — ON ITS OWN, ANIMATED IN  —  added 2026-08-27
   ----------------------------------------------------------------------------
   After a successful send the section drops "Let's make your business
   visible." entirely (the whole .contact-intro is skipped in index.php, not
   hidden with CSS - there is no reason to ship a pitch to somebody who has
   just accepted it) and .contact gets .contact-done.

   .contact is a 1fr 1fr grid. With one child left it would sit in the first
   column with an empty half beside it, so the confirmation state collapses to
   a single centred column and the panel's own 30ch cap is lifted.

   THE ANIMATION. Not the site's character-by-character reveal, for two
   reasons: that machinery is scoped to #work and #promise, and the good-news
   line carries .heading-gradient, which the splitter deliberately skips (a
   split gradient cannot be painted as one sweep). So this is a staggered rise
   instead, using the same easing and blur the .work-char reveal uses, so it
   belongs to the same family without pretending to be the same effect.

   Runs on load rather than on scroll - no data-reveal anywhere near it. A
   confirmation that waits for a scroll is a confirmation the visitor may
   never see, which is the bug this whole panel replaced.

   TO REVERSE: delete this block and drop the two PHP conditionals around
   .contact-intro in index.php.
   ========================================================================== */

.contact-done {
  grid-template-columns: 1fr;
  justify-items: center;
  text-align: center;
}

.contact-done .form-done {
  max-width: 46ch;
}

.contact-done .form-done-head {
  font-size: clamp(40px, 6vw, 78px);
}

.contact-done .form-done-copy {
  max-width: 48ch;
  margin-inline: auto;
  font-size: 17px;
}

@keyframes skyline-done-in {
  from {
    opacity: 0;
    transform: translate3d(0, 26px, 0);
    filter: blur(6px);
  }
  to {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}

.contact-done .form-done > * {
  opacity: 0;
  animation: skyline-done-in .9s cubic-bezier(.16, 1, .3, 1) forwards;
}

.contact-done .form-done > :nth-child(1) { animation-delay: .10s; }
.contact-done .form-done > :nth-child(2) { animation-delay: .22s; }
.contact-done .form-done > :nth-child(3) { animation-delay: .40s; }
.contact-done .form-done > :nth-child(4) { animation-delay: .54s; }

@media (prefers-reduced-motion: reduce) {
  .contact-done .form-done > * {
    opacity: 1;
    animation: none;
  }
}

/* ============================================================================
   FREE WEBSITE CONFIRMATION — ON ITS OWN, ANIMATED IN  —  added 2026-08-27
   ----------------------------------------------------------------------------
   The same treatment #contact got, applied to #free-website: on a successful
   application "One good idea. One website. FREE." is skipped in index.php
   and the section carries .free-offer-done.

   .free-offer is a 1fr .85fr grid, so the leftover column had to be collapsed
   here too, otherwise the panel would sit in the wide column with the narrow
   one empty beside it.

   Reuses the skyline-done-in keyframe defined in the #contact block above
   rather than declaring a second identical one - if the entrance is ever
   retimed, both confirmations should change together.

   This panel keeps .form-done-light, so it stays ink-on-white; only the
   layout and the entrance are shared with the dark one.

   TO REVERSE: delete this block and drop the two PHP conditionals around
   .free-copy in index.php.
   ========================================================================== */

.free-offer-done {
  grid-template-columns: 1fr;
  justify-items: center;
  text-align: center;
}

.free-offer-done .form-done {
  max-width: 46ch;
}

.free-offer-done .form-done-head {
  font-size: clamp(40px, 6vw, 78px);
}

.free-offer-done .form-done-copy {
  max-width: 48ch;
  margin-inline: auto;
  font-size: 17px;
}

.free-offer-done .form-done > * {
  opacity: 0;
  animation: skyline-done-in .9s cubic-bezier(.16, 1, .3, 1) forwards;
}

.free-offer-done .form-done > :nth-child(1) { animation-delay: .10s; }
.free-offer-done .form-done > :nth-child(2) { animation-delay: .22s; }
.free-offer-done .form-done > :nth-child(3) { animation-delay: .40s; }
.free-offer-done .form-done > :nth-child(4) { animation-delay: .54s; }

@media (prefers-reduced-motion: reduce) {
  .free-offer-done .form-done > * {
    opacity: 1;
    animation: none;
  }
}

/* ============================================================================
   ANCHOR LANDINGS — TIGHT UNDER THE HEADER  —  added 2026-08-27
   ----------------------------------------------------------------------------
   html carries scroll-padding-top:80px, which clears the fixed header (84px
   loose, 64px once .is-stuck, which is the state it is always in by the time
   a jump lands). That was the ONLY offset in play, and it aligns the SECTION,
   not the heading inside it. Every section then adds its own top padding on
   top of that, so clicking Pricing put the h2 roughly 200px down the screen -
   80px of header clearance plus 120px of .section-pad. That is the gap.

   Fixed by giving each anchored section a NEGATIVE scroll-margin-top equal to
   the padding above its heading. The maths, with P for that padding:

     snap area top   = section top - scroll-margin-top
     browser aligns  snap area top  to  scroll-padding-top (80px)
     so heading lands at  80 + scroll-margin-top + P
     set scroll-margin-top = -P  and the heading lands at 80px. Every time.

   80px from the top of the viewport, against a 64px stuck header, leaves 16px
   of air. Tight, and consistent across sections whatever their own padding.

   Each section declares only its own padding as --heading-inset and the shared
   rule does the arithmetic, so if a section's padding is ever retuned, its
   anchor follows automatically - the number lives in one place per section
   instead of being duplicated into a hand-computed offset that silently rots.

   #process (.research) is the exception: it has no top padding at all, it is
   a flex box with min-height:790px centring its content, so its inset is 0
   and it lands flush. Its copy still sits mid-section because that section is
   designed to be vertically centred - that is layout, not anchor offset.

   #top is on <main> and is left alone, so the logo still returns to the true
   top of the page rather than 80px down it.

   TO REVERSE: delete this block. scroll-padding-top:80px on html takes back
   over on its own and behaves exactly as it did before.
   ========================================================================== */

section[id] {
  scroll-margin-top: calc(0px - var(--heading-inset, 0px));
}

/* Order matters: .work also carries .section-pad, and both are one class deep,
   so the later declaration is the one that wins. */
.section-pad { --heading-inset: 120px; }
.promise     { --heading-inset: clamp(90px, 8vw, 120px); }
.free-offer  { --heading-inset: 100px; }
.research    { --heading-inset: 0px; }
.work        { --heading-inset: clamp(58px, 5.4vw, 76px); }

@media (max-width: 760px) {
  .section-pad { --heading-inset: 82px; }
  .promise     { --heading-inset: 72px; }
  .free-offer  { --heading-inset: 80px; }
  .work        { --heading-inset: 46px; }
}

/* ============================================================================
   HORIZONTAL SCROLL — THE FULL-BLEED WORK GRID  —  added 2026-08-27
   ----------------------------------------------------------------------------
   .work-grid goes full width with `margin-inline: calc(50% - 50vw)`, the
   standard break-out trick. It has one flaw and this page hits it: 50% is
   half the container's CONTENT width, which excludes the vertical scrollbar,
   while 50vw is half the VIEWPORT, which includes it. On any desktop browser
   with a classic scrollbar the two disagree by half its width at each edge, so
   the grid ends up about 15px wider than the page and the document gains a
   small horizontal scroll. It is exactly the "not much, but it should not be
   there" amount.

   Replaced with the section's own horizontal padding, negated. .work carries
   .section-pad, whose padding-inline is clamp(22px,5vw,84px) (20px on phones),
   so pulling out by precisely that lands the grid flush with the section's
   edges - which ARE the viewport edges, the section being a full-width block.
   No vw, nothing that can disagree with the scrollbar, exact at every width.

   If .section-pad's horizontal padding is ever retuned, THIS VALUE MUST MOVE
   WITH IT, which is the one cost of the swap.

   TO REVERSE: delete this block; the calc(50% - 50vw) rule above takes back
   over, scrollbar bug included.
   ========================================================================== */

.work-grid {
  margin-inline: calc(-1 * clamp(22px, 5vw, 84px));
}

@media (max-width: 760px) {
  .work-grid {
    margin-inline: -20px;
  }
}

/* ============================================================================
   RESEARCH IMAGE — FADING OUT TO THE RIGHT WHITE  —  added 2026-08-27
   ----------------------------------------------------------------------------
   The bottom of the photograph looked slightly grey against the section under
   it, and it was: .research:after's vertical layer ends on var(--paper),
   #f7f7f3, because <body> is --paper and .research has no background of its
   own. Directly beneath it #free-website is background:#fff. So the image was
   fading correctly into the PAGE and then meeting a whiter section - a four-
   point step, invisible in isolation, obvious as a line across the join.

   Two changes, both to the vertical layer only. The horizontal layer, which
   whitens the right-hand side behind the copy, is repeated unaltered because
   `background` is a shorthand.

   1. The bottom stop is #fff instead of --paper, so the last row of pixels
      matches the section it meets and the seam disappears.

   2. The ramp starts at 84% rather than 88% and passes through a half-white
      at 93%, so it arrives gradually rather than in the final tenth. This is
      what stops it reading as a wash laid ON the photograph.

   The image is NOT touched: .research-image's own mask still does the real
   dissolve. This only corrects the colour the dissolve lands on, so nothing
   is dimmed that was not already fading.

   The TOP stop stays --paper: above is #pricing at #0c0d0e, a deliberate hard
   change of section, not a blend.

   Desktop only. On phones the strip below the image is the copy sitting on
   --paper, so there --paper is the correct destination and already right.

   TO REVERSE: delete this block.
   ========================================================================== */

@media (min-width: 761px) {
  .research:after {
    background:
      linear-gradient(90deg,
        transparent 0%,
        transparent 45%,
        #ffffff94 61%,
        #fffffff0 100%),
      linear-gradient(180deg,
        var(--paper) 0%,
        transparent 9%,
        transparent 84%,
        #ffffff87 93%,
        #fff 100%);
  }
}

/* ============================================================================
   HERO INTRO — SEQUENCED  —  added 2026-08-27
   ----------------------------------------------------------------------------
   .hero-copy used to arrive as ONE block: hero-copy-in .75s at 1.56s, the
   whole thing lifting 30px together. This staggers its parts so the hero
   assembles - eyebrow, headline, the gradient line, the paragraph, the
   buttons - over about a second.

   NOTHING HERE COSTS SEO OR LOAD TIME, which was the condition:

   - No JavaScript. Pure CSS keyframes, so there is no new script, no DOM
     rewriting, and nothing to parse or execute. The character-splitting
     machinery used by #work and #promise was deliberately NOT reused here:
     it rewrites text into per-letter spans at runtime, and the one heading
     that must never depend on JS is the h1 above the fold.

   - The HTML is unchanged apart from one <span> around "Your idea", which
     exists only so the two headline lines can arrive separately. The text,
     the h1, its id and the reading order are all exactly as they were, so
     what a crawler or an LLM fetches is identical.

   - LCP IMPROVES. The headline is the likely LCP element. It was gated
     behind the curtain intro at 1.56s; with the intro cut to 1s and the
     sequence moved up with it, "Your idea" now starts at 1.17s. The
     largest text paints EARLIER than before this was built, not later.
     (Original note, kept for the reasoning: the headline was already
     gated behind the intro, so the sequence is built around its timing
     rather than pushing everything back to make room; everything after
     it is smaller copy that does not bid for LCP.)

   - Only opacity and transform are animated, both compositor-only, so there
     is no layout work per frame and no CLS: every element occupies its final
     space from the first frame and only its paint moves.

   THE GRADIENT LINE IS A SPECIAL CASE. .hero h1 em already runs
   `hero-em-drift 10s linear infinite`, and `animation` is a shorthand - a
   second declaration would have silently killed the drift. Its entrance is
   therefore appended to the same list rather than declared separately. It is
   also given opacity ONLY, no transform: the line is painted with
   background-clip:text over a 200%-wide drifting gradient, and transforming
   an inline box like that risks changing where the gradient sits. It fades
   up while the line above it rises, which reads as one gesture anyway.

   Resting state is the FINISHED state - there is no opacity:0 base rule
   anywhere. A browser that skips these, or a visitor who has asked for less
   motion, sees the finished hero immediately. Same fallback logic as the
   curtain panels above.

   TO REVERSE: delete this block. `.hero .hero-copy`'s single hero-copy-in
   animation further up the file takes back over unchanged, and the <span>
   in index.php becomes inert.
   ========================================================================== */

@keyframes hero-part-in {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: none; }
}

@keyframes hero-em-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (prefers-reduced-motion: no-preference) {

  /* The block animation is stood down; its children carry it now. */
  .hero .hero-copy {
    animation: none;
  }

  .hero .hero-copy > .eyebrow,
  .hero .hero-copy > .hero-intro,
  .hero .hero-copy > .hero-actions,
  .hero h1 .hero-line {
    animation: hero-part-in .62s cubic-bezier(.16, 1, .3, 1) both;
  }

  .hero h1 .hero-line {
    display: inline-block; /* transforms do not apply to a bare inline box */
  }

  .hero .hero-copy > .eyebrow      { animation-delay: 0.95s; }
  .hero h1 .hero-line              { animation-delay: 1.17s; }
  .hero .hero-copy > .hero-intro   { animation-delay: 1.61s; }
  .hero .hero-copy > .hero-actions { animation-delay: 1.83s; }

  /* Drift first, entrance second - see the note above on the shorthand. */
  .hero h1 em {
    animation:
      hero-em-drift 10s linear infinite,
      hero-em-in .8s cubic-bezier(.16, 1, .3, 1) 1.39s both;
  }
}

/* ============================================================================
   HERO INTRO — EVERY FIFTH VISIT  —  added 2026-08-27
   ----------------------------------------------------------------------------
   The curtain intro is a first-impression device. It is very good the first
   time and an obstacle the tenth, so it now plays on visits 1, 6, 11, 16 and
   so on, and is skipped in between. The counting is a small inline script in
   index.php's <head> (search "INTRO CADENCE"), which adds .no-intro to <html>
   before the first paint; everything below is what that class switches off.

   SKIPPING IS SAFE BECAUSE THE RESTING STATE IS ALREADY THE FINISHED ONE.
   None of these elements has an opacity:0 or covering base rule - the
   curtains sit at translateY(-100%), .hero-image at opacity:1,
   .curtain-mark-fill at clip-path:inset(0 0 0 0), and the hero copy has no
   hidden base at all. So `animation: none` does not need a matching set of
   "finished" declarations; removing the animation IS the finished hero. That
   is the same property the prefers-reduced-motion fallback already relied on.

   .hero h1 em IS THE EXCEPTION and must not be blanket-cleared: it carries
   TWO animations, the infinite hero-em-drift that makes the gradient move
   plus its one-shot entrance. `animation: none` would kill the drift as
   well, permanently, on four visits out of five. It is therefore re-declared
   with the drift alone rather than switched off.

   A visit is a SESSION, not a page load - see the note in the script. A
   visitor who reloads twice or bounces to /elevate/ and back has had one
   visit, not three.

   If storage is blocked, the script throws, .no-intro is never added, and
   every visit plays the intro exactly as before this existed.

   TO REVERSE: delete this block and the script in index.php. Nothing else
   references .no-intro.
   ========================================================================== */

.no-intro .hero .hero-curtain--one,
.no-intro .hero .hero-curtain--two,
.no-intro .hero-image,
.no-intro .hero .curtain-mark,
.no-intro .hero .curtain-mark-fill,
.no-intro .hero .hero-copy,
.no-intro .hero .hero-copy > .eyebrow,
.no-intro .hero .hero-copy > .hero-intro,
.no-intro .hero .hero-copy > .hero-actions,
.no-intro .hero h1 .hero-line {
  animation: none;
}

/* Drift kept, entrance dropped - see the note above. */
.no-intro .hero h1 em {
  animation: hero-em-drift 10s linear infinite;
}


/* ============================================================================
   PROMISE RULE GRADIENT  —  added 2026-08-28
   ----------------------------------------------------------------------------
   The three hairlines above 01 / 02 / 03 in the #promise block now carry the
   same animated shopfront gradient as the section subheadings
   (.heading-gradient) and the hero wordmark, so the rules read as part of the
   same family rather than as neutral ink.

   Same five stops, same palindrome at 200% width, same 10s linear drift
   (skyline-subhead-drift), so all three treatments stay in step.

   TWO RULES, BECAUSE THERE ARE TWO LINES:
   1. `.promise-points article:before` is the real one on a live page — main.js
      adds .promise-characters-ready, which zeroes the border and sweeps this
      1px pseudo-element in with scaleX. The drift animation runs on
      background-position, which is independent of that transform transition,
      so the reveal sweep is untouched.
   2. `.promise-points article` keeps the plain border-top for the no-JS /
      pre-ready state. A border cannot take a background, so it uses
      border-image with a 1px slice instead - static, no drift, but the same
      colours. It disappears on its own once border-top:0 lands.

   TO REVERSE: delete this whole block. The .promise-points article border and
   the var(--ink) pseudo-element background above take back over untouched.
   ========================================================================== */

.promise-points article {
  border-image: linear-gradient(
      90deg,
      #EF6A16 0%,
      #F69C11 14%,
      #EFC74F 28%,
      #29B4CF 50%,
      #EFC74F 72%,
      #F69C11 86%,
      #EF6A16 100%) 1;
}

.promise-characters-ready .promise-points article:before {
  background: linear-gradient(
      90deg,
      #EF6A16 0%,
      #F69C11 14%,
      #EFC74F 28%,
      #29B4CF 50%,
      #EFC74F 72%,
      #F69C11 86%,
      #EF6A16 100%);
  background-size: 200% 100%;
  animation: skyline-subhead-drift 10s linear infinite;
}

/* Reduced-motion: keep the colours, stop the drift - matches .heading-gradient. */
@media (prefers-reduced-motion: reduce) {
  .promise-characters-ready .promise-points article:before {
    animation: none;
    background-position: 50% center;
  }
}


/* ============================================================================
   PROCESS RULE GRADIENT  —  added 2026-08-28
   ----------------------------------------------------------------------------
   Extends the PROMISE RULE GRADIENT block above to the "Before design" section
   (#process, "We learn your world / before we shape it."). The strong rule
   sitting above the 01-04 process list now carries the same animated shopfront
   gradient, so the two numbered-list sections match.

   ONLY THE TOP RULE. .process-list has two kinds of line: this near-black
   var(--ink) rule at the head of the list, and the four pale #cfd2d0 dividers
   between rows (.process-list li border-bottom). Those are deliberately a
   lower tier - gradient on all five would read as noise - so only the strong
   one is treated, which is also the exact counterpart of the promise rules.

   PSEUDO-ELEMENT, NOT border-image: this list has no JS line-reveal (unlike
   the promise blocks, its <li>s just fade in), so the line could have stayed a
   plain border. But a border cannot take a background, and a background cannot
   drift on a border - so it moves to a 1px :before, which lets it reuse
   skyline-subhead-drift at the same 10s timing as the subheadings, the hero
   wordmark and the promise rules. All four stay in step.

   TO REVERSE: delete this whole block. The original
   `.process-list { border-top: 1px solid var(--ink) }` takes back over.
   ========================================================================== */

.process-list {
  position: relative;
  border-top: 0;
}

.process-list:before {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  height: 1px;
  background: linear-gradient(
      90deg,
      #EF6A16 0%,
      #F69C11 14%,
      #EFC74F 28%,
      #29B4CF 50%,
      #EFC74F 72%,
      #F69C11 86%,
      #EF6A16 100%);
  background-size: 200% 100%;
  animation: skyline-subhead-drift 10s linear infinite;
}

/* Reduced-motion: keep the colours, stop the drift - matches .heading-gradient. */
@media (prefers-reduced-motion: reduce) {
  .process-list:before {
    animation: none;
    background-position: 50% center;
  }
}


/* ============================================================================
   PORTFOLIO CARD TAGS -> DESCRIPTIVE SENTENCES  —  added 2026-08-28
   ----------------------------------------------------------------------------
   The three-hook tag line under each card title (see PORTFOLIO CARD TAGS
   above) became one descriptive sentence per site, written from the live
   site's own copy. Same element, same class, same place in the reveal
   sweep - only the copy and the type treatment change here.

   WHY THE TYPE HAD TO CHANGE: the original was styled as a LABEL - wide
   .045em tracking and a 34ch measure, which suits "art & jewellery ·
   ecommerce · catalogue" but fights a real sentence. Tracking comes back to
   near-normal so the line reads as prose, the measure widens so the
   sentences land in two lines rather than three, and line-height opens a
   little to carry the extra line comfortably.

   These sit over a photographic mockup at .68 opacity, so the measure is
   also a legibility limit, not just a typographic one - much wider and the
   line runs into the busy part of the screenshot behind it.

   TO REVERSE: delete this block and restore the seven tag strings in
   index.php (see index.php.bak-2026-08-28-card-tag-sentences).
   ========================================================================== */

.work-tags {
  letter-spacing: .012em;
  line-height: 1.5;
  max-width: 52ch;
}

@media (max-width: 760px) {
  .work-tags {
    letter-spacing: .008em;
    max-width: 34ch;
  }
}


/* ============================================================================
   IDEA FORM — GRADIENT BORDER  —  added 2026-08-28
   ----------------------------------------------------------------------------
   The free-website form's outer edge now carries the same animated shopfront
   gradient as the subheadings, the promise rules and the process rule. Same
   stops, same palindrome, same 10s skyline-subhead-drift, so it stays in step
   with everything else on the page.

   WHY A MASKED RING AND NOT A BORDER OR border-image:
   - A border cannot take a background, so it cannot hold a gradient.
   - border-image can, but it ignores border-radius - the corners would come
     back square, and this card is a 20px radius.
   - The usual padding-box/border-box two-layer background trick would work,
     but it needs an OPAQUE inner layer, and this form is deliberately
     translucent (#ffffffed over backdrop-filter: blur(10px)). An opaque
     fill would kill the frosted glass; a translucent one would let the
     gradient wash through the whole card instead of just its edge.

   So the gradient goes on an ::after covering the border box, and a two-layer
   mask (everything, XOR the content box) punches out the middle, leaving only
   the 1px ring. The frosted background underneath is untouched.

   inset:-1px is exactly the border box: the element keeps its 1px border, so
   expanding the padding box by 1px on every side lands on the border edge,
   where the outer radius is the declared 20px.

   FREEZE WHILE FILLING IT IN: :focus-within pauses the drift the moment a
   field takes focus, and animation-play-state holds it at the position it
   reached rather than snapping back to the start. Deliberately focus-within
   and not a keystroke listener - motion at the edge of vision is what
   distracts while you type, and pausing for the whole time someone is
   working in the form is calmer than a border that stops and restarts around
   every burst of typing. It also needs no JS, no timers and no class churn.

   FALLBACK: the whole thing sits inside @supports for mask-composite. Where
   that is missing the original grey #d9dcda border is left alone, so the card
   never ends up with no edge at all.

   TO REVERSE: delete this whole block. The .idea-form border further up the
   file is untouched and takes back over.
   ========================================================================== */

@supports ((mask-composite: exclude) or (-webkit-mask-composite: xor)) {
  .idea-form {
    border-color: transparent;
  }

  .idea-form:after {
    content: "";
    position: absolute;
    inset: -1px;
    z-index: 1;
    border-radius: 20px;
    padding: 1px;
    background-image: linear-gradient(
        90deg,
        #EF6A16 0%,
        #F69C11 14%,
        #EFC74F 28%,
        #29B4CF 50%,
        #EFC74F 72%,
        #F69C11 86%,
        #EF6A16 100%);
    background-size: 200% 100%;
    -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
            mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask-composite: exclude;
    pointer-events: none;
    animation: skyline-subhead-drift 10s linear infinite;
  }

  /* Hold still while someone is filling the form in. */
  .idea-form:focus-within:after {
    animation-play-state: paused;
  }

  @media (max-width: 760px) {
    /* .idea-form drops to 20px padding here; the ring is unaffected, but the
       radius is declared rather than inherited, so it is restated for clarity
       if that radius ever changes on phones. */
    .idea-form:after {
      border-radius: 20px;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .idea-form:after {
      animation: none;
      background-position: 50% center;
    }
  }
}


/* ============================================================================
   IDEA FUND SMALL PRINT — SIZE AND SPACING  —  added 2026-08-28
   ----------------------------------------------------------------------------
   "It's one a month, so this is an application rather than a prize draw..."
   was 11px and sat flush against the paragraph above it, because the base
   reset zeroes every margin and nothing put one back.

   Now 15px against the 17px paragraph above - a step down rather than a drop
   to legal-notice size. It is a genuine caveat about how the offer works, so
   it should be comfortably readable; it just shouldn't compete with the copy
   it qualifies. 15px above it opens the gap without breaking the two apart.

   STILL !important: .free-copy>p:not(.section-label) is (0,2,1) and sets
   font-size:17px, which out-specifies this (0,2,0) selector - the same reason
   the original 11px rule needed it. line-height:1.65 keeps coming from that
   rule, which at 15px gives a comfortable 24.75px.

   On phones the paragraph above drops to 15px, so this steps to 14px to keep
   the same relationship rather than matching it exactly.

   TO REVERSE: delete this block. The .free-copy .small-print rule further up
   the file (11px, no margin) takes back over.
   ========================================================================== */

.free-copy .small-print {
  margin-top: 15px;
  font-size: 15px !important;
}

@media (max-width: 760px) {
  .free-copy .small-print {
    font-size: 14px !important;
  }
}


/* ============================================================================
   WORK HEADING — ALIGN WITH EVERY OTHER SECTION  —  added 2026-08-28
   ----------------------------------------------------------------------------
   "Selected work / Web design built to fit." was the only section heading on
   the page not sitting on the site's left margin. Measured live at a 500px
   viewport: pricing, process, easy-editing, FAQ, contact and free-website
   labels all start at 20px (the .section-pad padding); this one started at
   88px - the same 20px plus an extra 68px of its own.

   THE 68px WAS AN ORPHAN, not a design choice. .work-heading carries
   `max-width:1380px; margin:auto 68px`, which was written to pair with the
   ORIGINAL .work-grid: `max-width:1380px; margin:auto`, a centred 12-column
   grid. The grid was later rebuilt as full-bleed rows
   (`margin-inline: calc(50% - 50vw)`, see the .work-grid override further up),
   and nothing went back to the heading - so it kept an inset that no longer
   had anything to line up with. On phones it was the most obvious, because
   .section-pad drops to 20px while the 68px stayed fixed.

   margin-inline rather than a new shorthand: three later rules set
   margin-bottom on .work-heading (clamp values plus a 60px phone case), and a
   shorthand here would wipe them.

   TO REVERSE: delete this block. The heading goes back to a 68px inset on
   every screen size.
   ========================================================================== */

.work-heading {
  margin-inline: 0;
}


/* ============================================================================
   PORTFOLIO CARD — GRADIENT PREFIX RULE AND CAPTION RHYTHM  —  added 2026-08-28
   ----------------------------------------------------------------------------
   A vertical gradient rule now runs down the left of each card's title and
   description, and the arrow sits further clear of them.

   WHY THE MARKUP GAINED A WRAPPER: .work-meta>span:first-child is a flex
   column holding THREE children - <b>, .work-tags and .work-cta. The rule is
   meant to span the first two only, so a border on that container would have
   run past the description and down the side of the arrow as well. The
   alternative was an absolutely positioned pseudo-element with its bottom
   computed from the arrow's clamp() height plus the column gap, which breaks
   the moment either of those is retuned. So <b> and .work-tags are now
   wrapped in .work-text (see index.php), the rule is that wrapper's own
   pseudo-element, and it is correct at any title or description length by
   construction. main.js splits .work-meta b and .work-tags by their own
   selectors, so the reveal sweep is unaffected by the extra element.

   VERTICAL GRADIENT, VERTICAL DRIFT: the shared skyline-subhead-drift moves
   background-position horizontally, which on a 3px-wide bar would show one
   flat colour. This bar runs the same stops at 180deg and needs its own
   keyframe to drift them DOWN the rule instead of across it. Same palindrome,
   same 10s loop, so it still reads as part of the family.

   SPACING: the title-to-description gap tightens to clamp(10px,1vw,15px) -
   they are one unit against the rule now, and the old shared
   clamp(12px,1.4vw,20px) let them drift apart. That shared gap still sets the
   distance from the text block to the arrow, and the arrow takes 25px on top
   of it, so the break before the call to action is clearly the largest space
   in the caption.

   TO REVERSE: delete this block and unwrap the seven .work-text spans in
   index.php (search "work-text").
   ========================================================================== */

@keyframes skyline-rule-drift-y {
  from { background-position: center 0%; }
  to   { background-position: center 200%; }
}

.work-text {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(10px, 1vw, 15px);
  padding-left: clamp(16px, 1.3vw, 22px);
}

.work-text:before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 3px;
  border-radius: 3px;
  background-image: linear-gradient(
      180deg,
      #EF6A16 0%,
      #F69C11 14%,
      #EFC74F 28%,
      #29B4CF 50%,
      #EFC74F 72%,
      #F69C11 86%,
      #EF6A16 100%);
  background-size: 100% 200%;
  animation: skyline-rule-drift-y 10s linear infinite;
}

/* Move the arrow clear of the copy - 25px on top of the column gap. */
.work-cta {
  margin-top: 25px;
}

@media (prefers-reduced-motion: reduce) {
  .work-text:before {
    animation: none;
    background-position: center 50%;
  }
}


/* ============================================================================
   PORTFOLIO CTA DISC — BIGGER  —  added 2026-08-28
   ----------------------------------------------------------------------------
   The white arrow disc was clamp(24px, calc(0.95vw + 20.5px), 28px) - 24px on
   a phone, capped at 28px however wide the screen got. That cap came from the
   2026-08-27 card-text-scale pass (see the block above, where the disc was
   deliberately shrunk); it is the smallest thing in a caption whose title
   runs to 72px, and it is the only part of the card you are meant to click.

   Now clamp(34px, calc(1.05vw + 28px), 44px): roughly 34px on a phone and
   44px on a wide desktop, so about 1.4x bigger at the small end and 1.6x at
   the large. Same clamp(min, calc(Nvw + Cpx), max) idiom as the rest of that
   pass, so the curve stays continuous with no breakpoint to jump at - these
   sit on a photograph with no reflow to hide a step.

   The arrow glyph inside is sized in percentages (44% of the disc), so it
   scales with the disc and needs nothing here. The hover nudge and the
   reveal offset are transforms, so they are unaffected too.

   Roughly:  320px 34 · 768px 36 · 1180px 40 · 1440px 43 · 1524px+ 44

   TO REVERSE: delete this block. The 24-28px clamp above takes back over.
   ========================================================================== */

.work-cta-arrow {
  width: clamp(34px, calc(1.05vw + 28px), 44px);
}


/* ============================================================================
   TIER PAGE — LIVE EXAMPLE  —  added 2026-09-03, promoted to its own section
   and moved earlier in the page 2026-09-03
   ----------------------------------------------------------------------------
   Each /launch, /elevate, /signature page can show one real site built at
   that level: a screenshot, its name, a one-line description and a link to
   visit it live. Page order is now hero -> "who it's for" -> this -> "what
   you get" -> compare -> FAQ -> CTA, so the proof sits right after the case
   for the tier and before the feature list, rather than buried at the foot of
   it. See tier-template.php, which only renders it when the tier's image file
   actually exists on disk (tier-data.php's 'example' key), so a tier can ship
   before its example does with no broken image.

   .tier-example IS the <section> now (it was a sub-block's own class before,
   nested inside .tier-includes) - border-top plus the shared .section-pad
   class is exactly how every sibling section on this page marks a divide
   (.tier-fit, .tier-compare, .tier-faq all do the same), so no extra
   margin/padding was added here; it would only have doubled up with
   section-pad's own vertical padding.

   IMAGE SOURCE: Elevate and Signature reuse the same 1400x1050 photographic
   mockups already used on the home page's portfolio grid
   (/portfolio/delmar-tile.webp, /portfolio/monroe-tile.webp) rather than a
   fresh flat screenshot - they are the same real sites, already photographed
   to the site's own standard, and reusing them costs nothing.

   LAYOUT: a two-column row - image left, name/blurb/link right - matching the
   proportions of .tier-fit above it (roughly 45/55) rather than the 50/50 of
   the .tier-includes dl grid, because the image needs more room to read than
   a stat pair does. aspect-ratio + object-fit:cover crops any source image to
   the same 4:3 box regardless of its native dimensions, so the block does not
   care whether a future replacement image is exactly 1400x1050.

   COLLAPSE TO ONE COLUMN — FIXED 2026-09-04, own rule now, not shared: this
   used to live in the @media (width <= 900px) block up near .tier-fit, on
   the theory that it belonged with the other tier-page grid collapses. It
   never actually applied: CSS breaks a same-specificity tie by source order,
   and the unconditional rule above sits LATER in this file than that media
   query, so it always won regardless of viewport width. Below 900px the
   right-hand column (name/blurb/"Visit the live site") was still being laid
   out at grid-template-columns: minmax(240px,460px) 1fr - a fixed-width
   image column plus a second column that refused to shrink - so on any phone
   under about 420px (depends on the tier name's length) the row ran wider
   than the screen and its right half sat off-canvas: no scrollbar, nothing
   visibly wrong until you looked for it, the text just wasn't there. Moving
   the override to directly beneath this rule guarantees it wins on source
   order the way it was always meant to.

   TO REVERSE: delete this block AND the media query directly below it. The
   section still renders correctly without it - tier-template.php's guard
   means nothing breaks either way.
   ========================================================================== */

.tier-example {
  display: grid;
  grid-template-columns: minmax(240px, 460px) 1fr;
  align-items: center;
  gap: clamp(28px, 4vw, 56px);
  border-top: 1px solid #dcdfda;
}

@media (width <= 900px) {
  .tier-example {
    grid-template-columns: 1fr;
    gap: 24px;
  }
}

.tier-example-shot {
  display: block;
  aspect-ratio: 4 / 3;
  border-radius: 20px;
  overflow: hidden;
  box-shadow: var(--shadow);
}

.tier-example-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  display: block;
  transition: transform .6s cubic-bezier(.2, .8, .2, 1);
}

.tier-example-shot:hover img {
  transform: scale(1.035);
}

.tier-example-kicker {
  margin: 0 0 10px;
  color: var(--coral);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
}

.tier-example-name {
  margin: 0 0 12px;
  color: var(--ink);
  font-size: clamp(23px, 2.1vw, 31px);
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.15;
}

.tier-example-blurb {
  max-width: 46ch;
  margin: 0 0 22px;
  color: #5c605e;
  font-size: 16px;
  line-height: 1.68;
}

.tier-example-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--ink);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 5px;
  text-decoration-thickness: 1px;
}

.tier-example-link .arrow {
  font-size: 13px;
  text-decoration: none;
}
