/* 3w — section-level styles. Depends on tokens.css + base.css.

   Cross-cutting safety pattern used everywhere below: the DEFAULT CSS
   state of any animated element is its final, fully-visible state. JS
   opts an element INTO a hidden starting state (by adding a class)
   before animating it back to that same default — never the reverse.
   If JS fails to run, or fails partway through, nothing is ever stuck
   invisible; the page is simply static instead of animated. */

/* ── Nav ──────────────────────────────────────────────────────────── */

/* backdrop-filter lives on ::before, not the header itself — filter and
   backdrop-filter both create a new containing block for any
   position:fixed descendant, which would trap the full-screen mobile
   menu panel inside the header's own 72px box instead of the
   viewport. A pseudo-element gets the same visual blur without being
   a real DOM ancestor of that panel. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 150;
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-base) var(--ease-out);
}
.site-header::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: color-mix(in srgb, var(--canvas) 80%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.site-header.is-scrolled { border-bottom-color: var(--hairline-color); }
@media (prefers-reduced-transparency: reduce) {
  .site-header::before { background: var(--canvas); backdrop-filter: none; -webkit-backdrop-filter: none; }
}

.nav-row {
  height: 72px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
}

.wordmark {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--step-1);
  letter-spacing: -0.02em;
  text-decoration: none;
  color: var(--text);
}
.wordmark .accent { color: var(--signal); }

.main-nav {
  display: none;
  align-items: center;
  gap: var(--sp-6);
}
@media (min-width: 1024px) { .main-nav { display: flex; } }
.main-nav a {
  font-size: var(--step--1);
  font-weight: 500;
  text-decoration: none;
  color: var(--text-secondary);
  transition: color var(--dur-fast) var(--ease-out);
}
/* Two hooks, because they mean different things. aria-current="page"
   is the page you are on. data-section="current" is the nav item whose
   section contains the page you are on — a case study highlights Work
   without claiming to be work.html, which is what a screen reader was
   being told before. */
.main-nav a[aria-current="page"],
.main-nav a[data-section="current"] { color: var(--text); }
/* Split out from the current-page rule: a state that only exists with a
   pointer should not be handed to a tap. */
@media (hover: hover) and (pointer: fine) {
  .main-nav a:hover { color: var(--text); }
}

.nav-actions { display: flex; align-items: center; gap: var(--sp-4); }
.nav-actions .btn { display: none; }
@media (min-width: 600px) { .nav-actions .btn { display: inline-flex; } }
/* The rule above hides the header CTA below 600px, where there is no room
   for it, but the mobile menu panel sits inside .nav-actions too, so its
   "Get a quote" was hidden on exactly the screens that need the menu. */
/* .mobile-menu-panel a (0,1,1) outranks .btn and .btn-solid, so the button
   also picked up the panel's display-size link type and --text colour:
   ink on blue. Restated at (0,2,0). */
.mobile-menu-panel .btn { display: inline-flex; font-family: var(--font-body); font-weight: 500; font-size: var(--step--1); letter-spacing: normal; padding-block: 0; }
.mobile-menu-panel .btn-solid { color: var(--paper); }

/* ── Theme toggle ─────────────────────────────────────────────────── */
/* nav.js reads/writes data-theme and sessionStorage; this only styles
   the button and swaps which icon shows. Default (no data-theme, no
   dark system preference) is light — sun shown, moon hidden. The two
   overrides below mirror the exact guarding tokens.css itself uses:
   a dark system preference flips the icons unless a manual choice
   says otherwise, and :root[data-theme="dark"] always wins. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border: var(--hairline);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.theme-toggle svg { width: 18px; height: 18px; }
.theme-toggle .icon-moon { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-sun { display: none; }
  :root:not([data-theme="light"]) .theme-toggle .icon-moon { display: block; }
}
:root[data-theme="dark"] .theme-toggle .icon-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: block; }

@media (hover: hover) and (pointer: fine) {
  .theme-toggle:hover { border-color: var(--signal); color: var(--signal); }
}

/* ── Buttons ──────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  padding: 0 var(--sp-6);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--step--1);
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
/* Scale, not a 1px nudge. A single pixel of travel on a 44px control is
   below the threshold where a press registers as acknowledged; scale is
   felt rather than seen, and it scales the label with it. */
.btn:active { transform: scale(0.97); }
.btn-solid { background: var(--signal); color: var(--paper); }
@media (hover: hover) and (pointer: fine) { .btn-solid:hover { background: var(--signal-hover); } }
.btn-outline { border-color: var(--hairline-color); color: var(--text); }
@media (hover: hover) and (pointer: fine) { .btn-outline:hover { border-color: var(--signal); color: var(--signal); } }
/* Hardcoded, not tokenised, for the same reason .bg-ink itself is:
   --paper and --ink-900 both flip with the theme, but the band under
   this button never does. In dark mode the tokens resolved to a near
   black fill on a near black band — 1.02:1, an invisible button. */
.btn-on-ink { background: #F7F5F1; color: #0B0B0C; }
/* The same idea for the blue band. Tokenised rather than hardcoded here
   because --on-signal and --signal already invert together: paper fill
   with blue text in light, near-black fill with lifted blue in dark.
   6.9:1 and 4.97:1 respectively. */
.btn-on-band { background: var(--on-signal); color: var(--signal); }

/* Text link with an arrow that nudges on hover — used for secondary
   CTAs where a full button would compete with the primary one. */
.link-arrow {
  display: inline-flex;
  align-items: center;
  /* Its line box is 21px, three short of the 24px minimum target size —
     the audit flagged this sitewide and every new arrow link inherited
     it. min-height rather than padding so nothing shifts around it. */
  min-height: 24px;
  gap: var(--sp-2);
  font-weight: 500;
  font-size: var(--step--1);
  text-decoration: none;
  color: var(--text);
  transition: transform var(--dur-fast) var(--ease-out); /* magnetic pull, main.js */
}
.link-arrow .arrow { transition: transform var(--dur-base) var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .link-arrow:hover .arrow { transform: translateX(4px); } }

/* ── Mobile menu — native <details>, JS-enhanced ─────────────────── */
/* Open/close, keyboard access, and every link is fully functional with
   zero JS via the native disclosure. nav.js only adds the focus trap,
   Escape-to-close, and the staggered link reveal (which is itself a
   plain CSS transition keyed off the [open] attribute — no JS needed
   for the animation either, only for the extra a11y behaviour). */

.mobile-menu { position: relative; }
@media (min-width: 1024px) { .mobile-menu { display: none; } }

.mobile-menu summary {
  list-style: none;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  position: relative;
  z-index: 201;
}
.mobile-menu summary::-webkit-details-marker { display: none; }
.mobile-menu summary::marker { content: ''; }

.mobile-menu summary .icon { position: relative; width: 20px; height: 12px; }
.mobile-menu summary .icon span {
  position: absolute;
  left: 0;
  right: 0;
  height: 1.5px;
  background: var(--text);
  transition: transform var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}
.mobile-menu summary .icon span:nth-child(1) { top: 0; }
.mobile-menu summary .icon span:nth-child(2) { top: 6px; }
.mobile-menu summary .icon span:nth-child(3) { top: 12px; }
.mobile-menu[open] summary .icon span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.mobile-menu[open] summary .icon span:nth-child(2) { opacity: 0; }
.mobile-menu[open] summary .icon span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

/* display:flex is scoped to [open] deliberately — the native UA
   stylesheet already hides a closed <details>'s non-summary content
   (display:none), and an unconditional display here would silently
   cancel that, leaving the full-screen panel painted over the page
   even while "closed". */
.mobile-menu[open] .mobile-menu-panel {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--canvas);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-1);
  padding: var(--page-pad);
  margin: 0;
}
.mobile-menu-panel a {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--step-3);
  letter-spacing: -0.02em;
  text-decoration: none;
  color: var(--text);
  padding-block: var(--sp-2);
}
/* Stagger keyed purely off the [open] attribute — works with zero JS. */
.mobile-menu[open] .mobile-menu-panel a {
  animation: menu-item-in var(--dur-base) var(--ease-out) both;
}
.mobile-menu[open] .mobile-menu-panel a:nth-child(1) { animation-delay: 60ms; }
.mobile-menu[open] .mobile-menu-panel a:nth-child(2) { animation-delay: 120ms; }
.mobile-menu[open] .mobile-menu-panel a:nth-child(3) { animation-delay: 180ms; }
.mobile-menu[open] .mobile-menu-panel a:nth-child(4) { animation-delay: 240ms; }
.mobile-menu[open] .mobile-menu-panel a:nth-child(5) { animation-delay: 300ms; }
@keyframes menu-item-in {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: translateY(0); }
}

.mobile-menu-meta {
  margin-top: var(--sp-8);
  padding-top: var(--sp-5);
  border-top: var(--hairline);
}

/* ── Skip link uses tokens already defined in base.css ───────────── */

/* ── Hero ─────────────────────────────────────────────────────────── */

.hero { padding-top: var(--sp-9); padding-bottom: var(--sp-10); }
/* Tighter than the old sp-10/sp-11 now the headline is roughly 190px
   rather than 130px: with the previous padding the hero ran to ~1430px
   and pushed "Get a quote" below the fold on a 1440x900 screen. The
   headline absorbed the height the padding used to provide. */
@media (min-width: 1024px) { .hero { padding-top: var(--sp-8); padding-bottom: var(--sp-9); } }

/* Generic 12/6/4-col row — reused by any section that needs to place
   content against the real grid, not just the hero. */
.content-grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
  column-gap: var(--grid-gutter);
  row-gap: var(--sp-9);
}

/* .hero-main used to be a .content-grid child spanning all 12 columns.
   The hero no longer uses that grid — it's now a plain block with a
   two-column .hero-foot beneath it — and leaving the span in place made
   .hero-main cover both foot tracks, pushing .hero-start onto its own
   row. It needs no column rule at all now. */

.hero-eyebrow { margin-bottom: var(--sp-5); }

.hero-headline {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--step-5);
  line-height: 0.95;
  letter-spacing: -0.03em;
  max-width: 13ch;
}
/* At --step-5 the headline tops out around 130px, which used barely
   two-thirds of the available width and left the whole first screen
   looking under-set. From 1024px up it scales with the viewport
   instead. The ceiling is deliberate: .container caps at
   --content-max, so the usable width stops growing at ~1312px however
   wide the screen gets, and measured at 192px the longest line
   ("earn their keep.") is ~1252px — the largest size that still
   clears it without wrapping. */
@media (min-width: 1024px) {
  .hero-headline {
    font-size: clamp(6rem, 13.3vw, 12rem);
    max-width: 16ch;
  }
}
.hero-headline .line { display: block; overflow: hidden; }
.hero-headline .line span { display: block; }
/* Only once JS has confirmed it can run the reveal does the headline
   get a hidden starting point to animate out of. */
.hero-headline.js-anim .line span {
  transform: translateY(110%);
  transition: transform var(--dur-slow) var(--ease-out);
}
.hero-headline.js-anim.is-revealed .line span { transform: translateY(0); }
.hero-headline.js-anim .line:nth-child(1) span { transition-delay: 0ms; }
.hero-headline.js-anim .line:nth-child(2) span { transition-delay: 90ms; }

/* ── Headline scroll-reveal ───────────────────────────────────────── */
/* Same clipped-box mask technique as the hero headline above, just
   applied by main.js to section h2/h1s on scroll instead of on load.
   main.js only ever wraps + adds these classes when it can also
   guarantee the reveal (IntersectionObserver + motion allowed), so
   the base, class-less state renders every heading fully visible. */
/* The mask that hides the text before it slides up. It has to clip the
   top, but clipping the bottom was slicing the descenders (and, on a
   two-line heading, a good chunk of the final line) clean off: display
   leading here is deliberately tight — 1.03 on page intros, 0.95 on the
   hero — which makes the line box shorter than the glyphs need, so they
   spill past the content box and straight into the clip.

   Padding the clip box down and pulling the same amount back off the
   layout gives the glyphs their room without moving anything.

   The hidden offset has to grow by the same amount, though. Overflow
   clips to the padding box, so on a single-line heading the old flat
   110% only cleared the content box by about 3px while the padding
   added 6 — the tops of the letters would have peeked below the mask
   before the reveal fired. Adding the padding to the offset keeps the
   text below the padding box at every heading size. */
.text-reveal { overflow: hidden; padding-bottom: 0.2em; margin-bottom: -0.2em; }
.text-reveal-inner {
  display: inline-block;
  transform: translateY(calc(110% + 0.2em));
  transition: transform var(--dur-slow) var(--ease-out);
}
.text-reveal.is-visible .text-reveal-inner { transform: translateY(0); }

.hero-sub {
  margin-top: var(--sp-6);
  font-size: var(--step-1);
  color: var(--text-secondary);
  max-width: 34ch;
}
.hero.js-anim .hero-sub {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
  transition-delay: 200ms;
}
.hero.js-anim.is-revealed .hero-sub { opacity: 1; transform: translateY(0); }

.hero-ctas {
  margin-top: var(--sp-7);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-5);
}
.hero.js-anim .hero-ctas {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
  transition-delay: 280ms;
}
.hero.js-anim.is-revealed .hero-ctas { opacity: 1; transform: translateY(0); }

/* ── Hero foot: copy + CTAs left, "start here" panel right ────────── */
.hero-foot {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-8);
  margin-top: var(--sp-8);
}
@media (min-width: 900px) {
  .hero-foot {
    /* Explicit width on the panel track rather than minmax(0, auto):
       with a 1fr sibling, an auto track whose min is 0 gets squeezed
       to zero and the panel silently wraps underneath. */
    grid-template-columns: minmax(0, 1fr) clamp(260px, 26vw, 360px);
    gap: var(--sp-10);
    align-items: end;
  }
}

.hero-start { display: flex; flex-direction: column; align-items: flex-start; }
.hero-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}
.hero-chip {
  display: inline-flex;
  align-items: center;
  height: 38px;
  padding: 0 var(--sp-5);
  border: var(--hairline);
  border-radius: 999px;
  font-size: var(--step--1);
  font-weight: 500;
  text-decoration: none;
  color: var(--text);
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .hero-chip:hover {
    background: var(--signal);
    border-color: var(--signal);
    color: var(--on-signal);
  }
}

/* Availability dot. --positive is a real token and reads on both the
   light and dark canvas, so it needs no per-theme handling. */
.status-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: var(--sp-2);
  border-radius: 999px;
  background: var(--positive);
  vertical-align: middle;
}
@media (prefers-reduced-motion: no-preference) {
  .status-dot { animation: status-pulse 2.4s var(--ease-in-out) infinite; }
}
@keyframes status-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

.hero-meta {
  margin-top: var(--sp-7);
  padding-top: var(--sp-5);
  border-top: var(--hairline);
  max-width: 40ch;
}
.hero.js-anim .hero-meta {
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
  transition-delay: 360ms;
}
.hero.js-anim.is-revealed .hero-meta { opacity: 1; }

/* ── Section index spine ──────────────────────────────────────────── */
/* The mono "01 / SERVICES" label every body section carries — it's the
   section's real position in the page, not decoration, so it starts
   counting at the first content section after the hero. */

.section-index {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  margin-bottom: var(--sp-6);
}
.section-index .num { color: var(--text); }

/* ── Scroll reveal ────────────────────────────────────────────────── */
/* Same invariant as the hero: default state is fully visible. reveal.js
   adds .js-anim (opting into a hidden start) only after confirming
   IntersectionObserver support and motion preference, then .is-visible
   once the element scrolls into view — once, never re-triggered. */

.js-reveal.js-anim > * {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
}
.js-reveal.js-anim.is-visible > * { opacity: 1; transform: translateY(0); }
.js-reveal.js-anim.is-visible > *:nth-child(1) { transition-delay: 0ms; }
.js-reveal.js-anim.is-visible > *:nth-child(2) { transition-delay: 60ms; }
.js-reveal.js-anim.is-visible > *:nth-child(3) { transition-delay: 120ms; }
.js-reveal.js-anim.is-visible > *:nth-child(4) { transition-delay: 180ms; }
.js-reveal.js-anim.is-visible > *:nth-child(5) { transition-delay: 240ms; }
.js-reveal.js-anim.is-visible > *:nth-child(6) { transition-delay: 300ms; }

/* ── Services — capability spine ─────────────────────────────────── */
/* Six numbered rows, not a 2x3 card grid — the point being made is
   that these six things are a chain (one team does all of them), and
   a grid of identical cards can't say that; a stacked, ordered list
   can. */

.services-intro { grid-column: 1 / -1; margin-bottom: var(--sp-8); }
@media (min-width: 1024px) { .services-intro { grid-column: span 8; } }
.services-intro h2 {
  font-size: var(--step-4);
  line-height: 1.05;
  font-weight: 500;
  letter-spacing: -0.02em;
  max-width: 20ch;
}

.service-row {
  display: grid;
  grid-template-columns: 40px 1fr;
  column-gap: var(--sp-5);
  row-gap: var(--sp-2);
  align-items: baseline;
  padding: var(--sp-5) 0;
  border-top: var(--hairline);
  position: relative;
  transition: background var(--dur-base) var(--ease-out);
}
.services-spine > .service-row:last-child { border-bottom: var(--hairline); }
@media (min-width: 1024px) {
  .service-row {
    grid-template-columns: 56px minmax(0, 3fr) minmax(0, 4fr) minmax(0, 2fr);
    column-gap: var(--sp-6);
    padding: var(--sp-6) var(--sp-5);
    margin-inline: calc(var(--sp-5) * -1);
  }
}

.service-row .num {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-secondary);
  transition: color var(--dur-base) var(--ease-out);
}
.service-row .name {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--step-2);
  letter-spacing: -0.01em;
  grid-column: 2;
}
@media (min-width: 1024px) { .service-row .name { grid-column: auto; } }
.service-row .desc {
  grid-column: 1 / -1;
  color: var(--text-secondary);
  max-width: 46ch;
}
@media (min-width: 1024px) { .service-row .desc { grid-column: auto; } }
.service-row .chip {
  grid-column: 1 / -1;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
@media (min-width: 1024px) {
  .service-row .chip { grid-column: auto; text-align: right; }
}

.service-row .rule {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 1px;
  width: 0%;
  background: var(--signal);
  transition: width var(--dur-base) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .service-row:hover { background: var(--surface); }
  .service-row:hover .num { color: var(--signal); }
  .service-row:hover .rule { width: 100%; }
}

/* ── Quote estimator ──────────────────────────────────────────────── */
/* Two panes: controls on the left, live result on the right. The
   result pane is the one that changes, so it gets the raised surface
   and the accent border to read as the "answer". */
.estimator {
  margin-top: var(--sp-9);
  border: var(--hairline);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.estimator-intro {
  padding: var(--sp-6) var(--sp-7) 0;
}
.estimator-intro h3 {
  margin-top: var(--sp-2);
  font-size: var(--step-1);
  font-weight: 500;
}
.estimator-controls { padding: var(--sp-6) var(--sp-7) var(--sp-7); }
@media (min-width: 900px) {
  .estimator {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: 'intro  result' 'controls result';
    align-items: start;
  }
  .estimator-intro { grid-area: intro; }
  .estimator-controls { grid-area: controls; }
  .estimator-result { grid-area: result; height: 100%; }
}

.est-field { border: 0; margin: 0 0 var(--sp-6); padding: 0; }
.est-field:last-child { margin-bottom: 0; }
.est-field > label,
.est-field legend {
  display: block;
  padding: 0;
  margin-bottom: var(--sp-3);
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--text);
}

/* Range input: styled explicitly on both engines because the default
   thumb/track render nothing like each other, and this control is the
   main thing people will actually grab. */
/* Renamed from #est-pages, which stopped existing when the estimator
   became the configurator — every rule in this block had been matching
   nothing, so the slider was rendering as an unstyled browser default.

   The padding is the other half of the original finding: a 2px track is
   a 2px target, so the input carries a 44px box around it and the track
   is drawn inside that. */
#cfg-pages {
  width: 100%;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  cursor: pointer;
  height: 44px;
}
#cfg-pages::-webkit-slider-runnable-track {
  height: 2px;
  background: var(--hairline-color);
}
#cfg-pages::-moz-range-track {
  height: 2px;
  background: var(--hairline-color);
}
#cfg-pages::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  margin-top: -9px; /* centres the thumb on a 2px track */
  border-radius: 999px;
  background: var(--signal);
  border: 0;
  transition: transform var(--dur-fast) var(--ease-out);
}
#cfg-pages::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border-radius: 999px;
  background: var(--signal);
  border: 0;
  transition: transform var(--dur-fast) var(--ease-out);
}
#cfg-pages:hover::-webkit-slider-thumb,
#cfg-pages:focus-visible::-webkit-slider-thumb { transform: scale(1.15); }
#cfg-pages:hover::-moz-range-thumb,
#cfg-pages:focus-visible::-moz-range-thumb { transform: scale(1.15); }

#cfg-pages-out {
  display: block;
  margin-top: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-secondary);
}

.est-check {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
  font-size: var(--step--1);
  color: var(--text-secondary);
  cursor: pointer;
}
.est-check:last-child { margin-bottom: 0; }
.est-check input { width: 16px; height: 16px; accent-color: var(--signal); cursor: pointer; }

.estimator-result {
  padding: var(--sp-7);
  background: var(--paper-2);
  border-top: var(--hairline);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
@media (min-width: 900px) {
  .estimator-result { border-top: 0; border-left: var(--hairline); }
}
.est-tier {
  margin-top: var(--sp-2);
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 500;
}
.est-price {
  font-family: var(--font-mono);
  font-size: var(--step-3);
  font-weight: 500;
  letter-spacing: -0.02em;
  margin-top: var(--sp-1);
}
.est-monthly {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-secondary);
  margin-top: var(--sp-2);
}
.est-includes {
  list-style: none;
  margin: var(--sp-5) 0 var(--sp-6);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  font-size: var(--step--1);
  color: var(--text-secondary);
}
.est-includes li { display: flex; align-items: center; gap: var(--sp-2); }

/* Blur-bridged swap. Without the blur you see two distinct strings
   overlapping mid-fade, which reads as a glitch; with it the eye reads
   one value turning into another. Kept to 3px — heavy blur is expensive,
   Safari especially. The panel dips rather than going fully transparent
   so the layout never appears to empty out. */
.est-tier, .est-price, .est-monthly, .est-includes {
  transition: opacity var(--dur-fast) var(--ease-out),
              filter var(--dur-fast) var(--ease-out);
}
.estimator-result.is-swapping .est-tier,
.estimator-result.is-swapping .est-price,
.estimator-result.is-swapping .est-monthly,
.estimator-result.is-swapping .est-includes {
  opacity: 0.4;
  filter: blur(3px);
}
.est-note {
  margin-top: var(--sp-4);
  font-size: var(--step--1);
  color: var(--text-secondary);
  max-width: 40ch;
}

/* ── Statement moment ─────────────────────────────────────────────── */
/* Full-height editorial type break. Uses the same clipped-box mask as
   the hero headline, but triggered on scroll by main.js rather than on
   load. Same safety convention as every other reveal here: the hidden
   starting position only ever applies once .js-anim is present, so
   with no JS (or no IntersectionObserver) the lines just render. */
.statement {
  min-height: 80vh;
  display: flex;
  align-items: center;
}
.statement-text {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--step-5);
  line-height: 0.95;
  letter-spacing: -0.03em;
  max-width: none; /* base.css caps p at 62ch; this is display type */
}
.statement-text .line { display: block; overflow: hidden; }
.statement-text .line span { display: block; }
.statement.js-anim .line span {
  transform: translateY(110%);
  transition: transform var(--dur-slow) var(--ease-out);
}
.statement.js-anim.is-visible .line span { transform: translateY(0); }
.statement.js-anim.is-visible .line:nth-child(1) span { transition-delay: 0ms; }
.statement.js-anim.is-visible .line:nth-child(2) span { transition-delay: 90ms; }
.statement.js-anim.is-visible .line:nth-child(3) span { transition-delay: 180ms; }

/* ── Oversized section numeral ────────────────────────────────────── */
/* Giant ghosted numeral set into a section's empty space, extending the
   existing "03 / TEAM" index motif to display scale. Pulled from the
   data-numeral attribute so it needs no extra markup, and aria-hidden
   is unnecessary for the same reason — generated content isn't exposed
   as text to assistive tech. Hidden below 1024px, where there is no
   spare horizontal room for it to occupy. */
.has-numeral { position: relative; overflow: hidden; }
.has-numeral > .container { position: relative; z-index: 1; }
.has-numeral::before {
  content: attr(data-numeral);
  position: absolute;
  top: 50%;
  right: var(--sp-8);
  transform: translateY(-50%);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(12rem, 26vw, 26rem);
  line-height: 1;
  letter-spacing: -0.05em;
  color: currentColor;
  opacity: 0.06;
  pointer-events: none;
  display: none;
}
@media (min-width: 1024px) { .has-numeral::before { display: block; } }

/* ── Capability marquee ───────────────────────────────────────────── */
/* Full-bleed scrolling strip of the stack we actually work in. Sits
   outside .container by design so it runs edge to edge, and is kept
   deliberately thin so it reads as a divider between two sections
   rather than as a section of its own (which would break the
   blue/paper alternation it exists to protect).

   The loop: .marquee-track holds two identical groups, so translating
   it -50% moves it by exactly one group's width and lands on a frame
   identical to the start — no visible jump. Each group carries a
   trailing gap on its last item (via the same ::after bullet every
   item gets) so spacing across the seam matches spacing everywhere
   else. */
.marquee {
  overflow: hidden;
  padding-block: var(--sp-5);
  /* Full-bleed out of the normal page flow width. */
  width: 100%;
}
.marquee-track {
  display: flex;
  width: max-content;
  animation: marquee-scroll 45s linear infinite;
}
.marquee-group {
  display: flex;
  align-items: center;
  gap: var(--sp-6);
  margin: 0;
  padding: 0 var(--sp-6) 0 0;
  list-style: none;
  flex-shrink: 0;
}
.marquee-group li {
  display: flex;
  align-items: center;
  gap: var(--sp-6);
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-secondary);
  white-space: nowrap;
}
/* Trailing separator on every item, so the seam between the two groups
   reads the same as any other join in the strip. Hardcoded rather than
   var(--signal) for the same reason .bg-ink hardcodes everything else:
   --signal is a dark blue in light mode and a lifted one in dark mode,
   but this strip is always dark, so the token would give the same
   surface two different accent colours depending on theme. This is the
   lifted value (4.97:1 on #0B0B0C) — the light-mode --signal is only
   2.62:1 here and reads as muddy. */
.marquee-group li::after {
  content: '·';
  color: #5C72FF;
}

@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* Pause on hover so anyone who wants to actually read it can. */
@media (hover: hover) and (pointer: fine) {
  .marquee:hover .marquee-track { animation-play-state: paused; }
}
/* Motion off: no scroll at all. The strip keeps its overflow:hidden,
   so this shows a static, readable slice of the list rather than
   animating — the content is fully listed on services.html anyway. */
@media (prefers-reduced-motion: reduce) {
  .marquee-track { animation: none; }
}

/* ── Process — Apple-style bento grid ─────────────────────────────── */
/* Four self-contained tiles rather than a hairline-divided list, with
   an asymmetric large/small span pattern (steps 1 and 4 run wide) —
   the same bento layout Apple uses on its feature pages. 24px radius
   matches .work-card-lg further down: two isolated, deliberate
   exceptions to the ≤4px-radius rule elsewhere, not a drift. Replaced
   the earlier scroll-jacked pinned/cross-fade version — a grid that
   shows every step at once doesn't combine with an interaction that
   only ever shows one. */

.process-intro { grid-column: 1 / -1; margin-bottom: var(--sp-8); }
@media (min-width: 1024px) { .process-intro { grid-column: span 8; } }
.process-intro h2 { font-size: var(--step-4); font-weight: 600; letter-spacing: -0.03em; line-height: 1.05; max-width: 18ch; }

.process-steps {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
}
@media (min-width: 600px) { .process-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 900px) { .process-steps { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 600px) { .process-step.is-wide { grid-column: span 2; } }

.process-step {
  background: var(--paper-2);
  border-radius: 24px;
  padding: var(--sp-7);
  transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.process-step-num {
  font-family: var(--font-mono);
  font-size: var(--step-2);
  font-weight: 500;
  color: var(--ink-900);
  margin-bottom: var(--sp-4);
  transition: color var(--dur-base) var(--ease-out);
}
.process-step.is-wide .process-step-num { font-size: var(--step-3); }
.process-step .mono-label { margin-bottom: var(--sp-2); }
.process-step h3 { margin: 0 0 var(--sp-2); font-size: var(--step-1); font-weight: 500; }
.process-step.is-wide h3 { font-size: var(--step-2); }
.process-step p { color: var(--text-secondary); max-width: 32ch; }

@media (hover: hover) and (pointer: fine) {
  .process-step:hover { transform: translateY(-4px); box-shadow: 0 16px 32px rgba(0, 0, 0, 0.08); }
  .process-step:hover .process-step-num { color: var(--signal); }
}

/* ── Featured work — horizontal card carousel ────────────────────── */
/* Deliberately breaks from the ≤4px-radius rule the rest of the site
   holds to — this is a direct, specific style request (Apple's own
   promo carousel), not a drift. Scoped entirely to .work-carousel so
   it doesn't leak the large-radius/rounded-card language anywhere
   else. Scroll-snap does the carousel mechanics; nav.js only adds the
   arrow button and hides it when there's nothing to scroll to. */

#work h2 { font-size: var(--step-4); font-weight: 600; letter-spacing: -0.03em; line-height: 1.05; }

/* The carousel deliberately breaks out of .container (moved to be a
   section-level sibling in the markup, not nested inside it) so the
   track can run fully edge-to-edge to the true right side of the
   viewport, not just to .container's own inset right edge — the last
   card ends flush with the screen, no trailing gap. The left edge
   still needs to line up with the heading above it (which stays inside
   .container), so it re-derives that same inset here: half the
   leftover width outside --content-max, plus --page-pad. */
.work-carousel-wrap {
  position: relative;
  margin-top: var(--sp-7);
  --carousel-inset: calc(max(0px, (100vw - var(--content-max)) / 2) + var(--page-pad));
}

.work-carousel {
  display: flex;
  gap: var(--sp-5);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: var(--carousel-inset);
  padding: 0 0 var(--sp-3) var(--carousel-inset);
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.work-carousel::-webkit-scrollbar { display: none; }

.work-card-lg {
  scroll-snap-align: start;
  flex: 0 0 auto;
  width: min(78vw, 460px);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-radius: 24px;
  overflow: hidden;
  border: var(--hairline);
}

.work-card-lg-text { padding: var(--sp-6) var(--sp-6) var(--sp-5); }
.work-card-lg .mono-label { color: var(--signal); }
.work-card-lg h3 { margin-top: var(--sp-3); font-size: var(--step-2); font-weight: 600; letter-spacing: -0.02em; }
.work-card-lg p { margin-top: var(--sp-2); color: var(--text-secondary); }

.work-card-lg-shot { aspect-ratio: 4 / 3; background: var(--paper-2); overflow: hidden; }
/* --dur-base, not --dur-slow: a hover is a response, and 520ms of it
   reads as lag rather than smoothness. */
.work-card-lg-shot img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform var(--dur-base) var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .work-card-lg:hover .work-card-lg-shot img { transform: scale(1.04); } }

/* Layout preview only — not real content. Dashed border and reduced
   opacity keep it visually unmistakable from an actual case study;
   it's a plain div, not a link, since there's nowhere real to go. */
.work-card-lg.is-placeholder {
  border-style: dashed;
  border-color: var(--ink-300);
  opacity: 0.55;
  cursor: default;
}
.work-card-lg.is-placeholder .mono-label { color: var(--text-secondary); }
.work-card-lg.is-placeholder .work-card-lg-shot {
  display: flex;
  align-items: center;
  justify-content: center;
}
.work-card-lg.is-placeholder .work-card-lg-shot svg { width: 32px; height: 32px; color: var(--ink-300); }

.work-carousel-next {
  display: none;
  position: absolute;
  top: 42%;
  right: var(--sp-5);
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  border-radius: 999px;
  background: var(--surface);
  border: var(--hairline);
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.work-carousel-next.is-visible { display: flex; }
.work-carousel-next svg { width: 18px; height: 18px; }

/* ── Pricing — value ladder ───────────────────────────────────────── */

.pricing-intro { grid-column: 1 / -1; margin-bottom: var(--sp-8); }
@media (min-width: 1024px) { .pricing-intro { grid-column: span 8; } }
.pricing-intro h2 { font-size: var(--step-4); font-weight: 600; letter-spacing: -0.03em; line-height: 1.05; }
.pricing-intro p { margin-top: var(--sp-4); color: var(--text-secondary); max-width: 52ch; }

.price-table { border-top: var(--hairline); }

.price-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-2);
  padding: var(--sp-4) 0;
  border-bottom: var(--hairline);
}
@media (min-width: 768px) {
  .price-row { grid-template-columns: minmax(0, 4fr) minmax(0, 3fr) minmax(0, 3fr); align-items: center; gap: var(--sp-5); }
}

.price-row.head {
  border-bottom-width: 1px;
  padding-top: var(--sp-6);
  padding-bottom: var(--sp-6);
}
/* The number leads. On a pricing page the price is the content, and it
   was set smaller than the body headings around it — a 55px mono figure
   under a 25px display name, so the label outranked the thing the whole
   page exists to state. Flipped: the tier name is a mono label, the
   price is display type at --step-4.

   Mono was wrong here for the same reason it was wrong on the card
   numerals. It is the right face for the comparison rows below, which
   are data and want to align in columns; a headline figure is not data,
   it is a headline. */
.price-row.head .tier-name {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--sp-3);
}
.price-row.head .tier-price {
  color: var(--text);
  font-family: var(--font-display);
  /* --step-4 does not fit. "€10,800" set at 80px measures 310px against
     a 283px content box inside the Grow panel, so the last digit sat
     outside the panel's own background — visible at every width, and my
     first attempt at this only patched a viewport range because I was
     testing document overflow, which text spilling inside a container
     never triggers. --step-3 measures ~212px in the same box.

     It also restores the hierarchy: the configurator's number is the
     one the page is built around and should stay the largest figure
     on it.

     Sized in cqw against the column rather than vw against the window,
     because the column is what it has to fit inside and the two are not
     proportional — the feature-name track takes 4fr of the row, so at
     800px the tier column is only ~179px while the viewport suggests
     far more. Every viewport-based value I tried fitted at one width and
     spilled at another. --step-3 stays as the ceiling. */
  font-size: min(var(--step-3), 18cqw);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 0.95;
}
/* The container the price is measured against. */
.price-row.head .tier-col { min-width: 0; container-type: inline-size; }
.price-row.head .tier-col > div { max-width: 100%; }

.price-row.head .tier-tag {
  display: inline-block;
  margin-top: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--signal);
}

/* The recommended tier was marked with --paper-2, which on a paper band
   is a shade you have to look for. It is a filled ink panel now, so the
   answer to "which one" is visible before a word is read. */
.price-row.head .is-chosen {
  /* Inverted against the page ground rather than hardcoded ink. A fixed
     #0B0B0C panel would sit on a #08080A band in dark mode at 1.03:1 and
     simply disappear — the same token-flip that has caught the tick, the
     focus ring and the ink button on this site already. --text and
     --canvas are exact opposites in both themes, so the panel is ink on
     paper in light and paper on ink in dark, with no second rule. */
  background: var(--text);
  color: var(--canvas);
  /* Vertical bleed only. The horizontal negative margin pulled the
     panel 24px past its own grid cell, which is invisible at 1440px and
     pushes the document 15px wider around 960px — the page scrolled
     sideways. It gains its width from padding now, inside the cell. */
  width: 100%;
  margin-block: calc(var(--sp-4) * -1);
  padding: var(--sp-5) var(--sp-4);
  display: flex;
  flex-direction: column;
  justify-content: center;
  border-radius: var(--radius-md);
}
.price-row.head .is-chosen .tier-price { color: var(--canvas); }
.price-row.head .is-chosen .tier-name,
/* The tag drops its blue here. Neither --signal nor the lifted variant
   clears 4.5:1 against both faces of an inverting panel, and it does not
   need colour to read as a tag: it is already uppercase mono with wide
   tracking, which is what marks it out. */
.price-row.head .is-chosen .tier-tag { color: var(--canvas); opacity: 0.72; }
@media (min-width: 768px) {
  .price-row.head .is-chosen { width: 100%; margin-block: calc(var(--sp-6) * -1); padding: var(--sp-6) var(--sp-5); }
}

/* The rows carried the detail people actually compare on and were set
   in the smallest type on the page, under two of the largest figures.
   Up a step, and the values take full-strength text rather than the
   muted colour meant for supporting copy — "Included" is the answer to
   the question, not a footnote to it. */
.price-row .feature-name { color: var(--text); font-size: var(--step-0); }
.price-row .tier-col {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  color: var(--text);
  font-size: var(--step-0);
}
/* Except "Not included", which should read as the absence it describes. */
.price-row .tier-col:has(.dash) { color: var(--text-secondary); }

/* The recommended tier was a panel on the head row and nothing
   underneath it, so Launch and Grow stopped being columns the moment you
   left the prices. The tint runs the full height of the table now, which
   is what makes a comparison table readable across a row: you follow a
   column, not a line of text.

   Only once the layout is actually two columns — below 768px each row
   stacks, and a full-width tint would just be a stripe. */
@media (min-width: 768px) {
  /* ".tier-col ~ .tier-col" is the second tier column, whatever follows
     it. :last-child was wrong: any row carrying an explanation panel has
     that panel as its last child, so the tint landed only on "Pages" —
     the one row without an explainer. */
  .price-row > .tier-col ~ .tier-col {
    /* Stretch, not centre. .price-row centres its cells, so the tinted
       cell was only as tall as its own text and the negative margin
       pulled up from that centred position — leaving an 8px gap where
       the column should have run continuously into the panel above. */
    align-self: stretch;
    align-items: center;
    /* --paper-2 is tuned as a light-mode band and lands at 1.08:1
       against the dark ground, where the column simply disappeared.
       Mixing from --text instead means the tint is always a wash of the
       foreground over the background — dark on paper, light on ink — so
       it reads in both. The flat value stays first as the fallback. */
    background: var(--paper-2);
    background: color-mix(in srgb, var(--text) 10%, transparent);
    margin-block: calc(var(--sp-4) * -1);
    padding: var(--sp-4) var(--sp-5);
  }
  /* No padding on the head cell: the panel inside it supplies its own,
     and any padding here insets the panel relative to the tinted cells
     below, which is what made the column look like two unrelated boxes
     stacked on each other. align-items: stretch so the panel fills the
     row rather than floating centred in it. */
  .price-row.head > .tier-col ~ .tier-col { background: none; padding-inline: 0; align-items: stretch; }
  /* Rounds off where the tint starts and stops. */
  .price-table .price-row:nth-of-type(2) > .tier-col ~ .tier-col { border-radius: var(--radius-md) var(--radius-md) 0 0; }
  .price-table .price-row:last-of-type > .tier-col ~ .tier-col { border-radius: 0 0 var(--radius-md) var(--radius-md); }
}

/* Every feature name is a button, and only a 14px icon said so. */
@media (hover: hover) and (pointer: fine) {
  .price-row:hover .why-icon { opacity: 1; }
  .price-row:hover .feature-name { color: var(--signal); }
}
/* Scoped to .price-row until now, which meant the identical markup in
   the Care band and the estimator's includes list matched nothing and
   rendered at 0x0 — those lists have been showing no marker at all.
   Unscoping fixes both. .bg-ink .tick still outranks this. */
.tick { display: inline-block; width: 16px; height: 2px; background: var(--signal); flex-shrink: 0; }
.dash { display: inline-block; width: 16px; height: 1px; background: var(--hairline-color); flex-shrink: 0; }

/* Below the two-column breakpoint, each feature row's two tier values
   stack directly on top of each other with nothing else distinguishing
   them — the header row's "Launch"/"Grow" context is gone by then, so
   each stacked value needs its own tiny label to still make sense. */
.tier-col-label { display: none; font-family: var(--font-mono); font-size: var(--step--1); letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-secondary); margin-right: var(--sp-1); }
@media (max-width: 767px) { .tier-col-label { display: inline; } }

.price-note { margin-top: var(--sp-6); color: var(--text-secondary); }
.price-note strong { color: var(--text); font-weight: 500; }

.care-band {
  margin-top: var(--sp-9);
  padding: var(--sp-7);
  background: var(--paper-2);
  border-radius: var(--radius-md);
  display: grid;
  gap: var(--sp-5);
}
@media (min-width: 768px) { .care-band { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) minmax(0, 2fr); align-items: center; padding: var(--sp-8); } }
.care-band .tier-name { font-family: var(--font-display); font-weight: 500; font-size: var(--step-1); }
.care-band .tier-price { font-family: var(--font-display); font-size: var(--step-4); font-weight: 600; letter-spacing: -0.03em; line-height: 1; margin-top: var(--sp-2); }
.care-band .tier-price .unit { font-size: var(--step--1); color: var(--text-secondary); }
.care-band ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); font-size: var(--step--1); color: var(--text-secondary); }
.care-band li { display: flex; align-items: center; gap: var(--sp-2); }

/* ── FAQ ──────────────────────────────────────────────────────────── */

.faq-intro { grid-column: 1 / -1; margin-bottom: var(--sp-7); text-align: center; }
.faq-intro .section-index { justify-content: center; }
.faq-intro h2 { font-size: var(--step-4); font-weight: 600; letter-spacing: -0.03em; line-height: 1.05; }

.faq-list { grid-column: 1 / -1; border-top: var(--hairline); }
@media (min-width: 1024px) { .faq-list { grid-column: 3 / 11; } }

.faq-item { border-bottom: var(--hairline); }
.faq-item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  padding-block: var(--sp-5);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--step-1);
  cursor: pointer;
  list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }

/* The +/- icon was the only thing that animated; the panel it controls
   snapped open. ::details-content plus interpolate-size animates to
   auto height with no wrapper element and no JS. Gated on @supports, so
   browsers without it keep the instant open rather than a broken one. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .faq-item::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size var(--dur-base) var(--ease-out),
                content-visibility var(--dur-base) allow-discrete;
  }
  .faq-item[open]::details-content { block-size: auto; }
}
.faq-item summary::marker { content: ''; }
.faq-item summary .icon {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  position: relative;
}
.faq-item summary .icon::before,
.faq-item summary .icon::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  background: var(--text);
  transition: transform var(--dur-base) var(--ease-out);
}
.faq-item summary .icon::before { width: 12px; height: 1.5px; transform: translate(-50%, -50%); }
.faq-item summary .icon::after { width: 1.5px; height: 12px; transform: translate(-50%, -50%); }
.faq-item[open] summary .icon::after { transform: translate(-50%, -50%) rotate(90deg) scaleY(0); }

.faq-item p { padding-bottom: var(--sp-5); margin: 0; max-width: 60ch; color: var(--text-secondary); }

/* ── Contact CTA band + footer ────────────────────────────────────── */

/* Top padding is deliberately smaller than the bottom: the preceding
   section already contributes its own full padding-bottom before the
   hairline, so a matching padding-top here would double the gap the
   same way plain .section-to-.section boundaries did (fixed below). */
/* 96/192 made this band float high in itself while every other band on
   the page runs 128/128. Evened up. */
.cta-band { padding-block: var(--sp-10); border-top: var(--hairline); }
/* Was a left-aligned stack, which left the right 55% of the band empty
   on every page that uses it. Two columns now: the line on the left, the
   actions on the right, meeting in the middle of the band. */
.cta-band .container { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-6); }
@media (min-width: 900px) {
  .cta-band .container {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    /* `gap` sets both axes, which put 128px between the heading and its
       own sub-line — they read as unrelated, and the buttons floated
       between them aligned to neither. The column still needs the wide
       gutter; the rows do not. */
    column-gap: var(--sp-10);
    row-gap: var(--sp-4);
  }
  .cta-band .cta-sub { grid-column: 1; }
  .cta-band .cta-actions { grid-column: 2; grid-row: 1 / span 2; margin-top: 0; flex-direction: column; align-items: flex-start; }
  .cta-band h2 { max-width: 18ch; }
}
/* Tight leading, for the reason the hero and page intros already have
   it: this heading inherited the body line-height (~1.55), which at 65px
   put a 101px line box around the type — 36px of dead space per line,
   and this heading runs to two lines on every page that uses it. */
/* --step-3, not --step-4. At --step-4 this closing line measured 80px on
   Services, Work, Pricing and About, where the page title measures 61px:
   the last band on the page outranked the first heading on it. */
.cta-band h2 { font-size: var(--step-3); font-weight: 600; letter-spacing: -0.03em; max-width: 14ch; line-height: 1.05; }
.cta-band .email-link {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--step-2);
  color: var(--signal);
}

/* The footer was four identical columns of small grey links with the
   wordmark shrunk to body size at the very bottom — a sitemap, not a
   footer. It has an anchor now: the mark at display scale, one line
   saying what the studio does, and the email as a real destination
   rather than the last item in the fourth column. The columns stay, but
   as support underneath rather than as the whole thing. */
.site-footer { border-top: var(--hairline); padding-block: var(--sp-9) var(--sp-7); }

.footer-lead {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
  padding-bottom: var(--sp-8);
  margin-bottom: var(--sp-8);
  border-bottom: var(--hairline);
}
@media (min-width: 900px) {
  .footer-lead {
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--sp-8);
  }
}
.footer-mark {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--step-4);
  letter-spacing: -0.04em;
  line-height: 0.8;
  color: var(--text);
  text-decoration: none;
}
.footer-mark .accent { color: var(--signal); }
.footer-line { color: var(--text-secondary); max-width: 34ch; }
/* The second conversion path on every page, and it used to be the last
   link in the fourth column at 13px. */
.footer-email {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: -0.02em;
  color: var(--signal);
  white-space: nowrap;
}

.footer-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-6); }
@media (min-width: 640px) { .footer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 900px) { .footer-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.footer-col a, .footer-col p { display: block; color: var(--text-secondary); font-size: var(--step--1); margin-top: var(--sp-2); }
/* 21px line boxes, three short of the minimum target — the same issue
   the arrow links had, in the densest list of links on the site. */
.footer-col a { padding-block: 2px; }
@media (hover: hover) and (pointer: fine) { .footer-col a:hover { color: var(--text); } }
.footer-col .mono-label { color: var(--text); }

.footer-bottom {
  margin-top: var(--sp-8);
  padding-top: var(--sp-5);
  border-top: var(--hairline);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
}
.footer-top { color: var(--text-secondary); text-decoration: none; padding-block: var(--sp-2); }
@media (hover: hover) and (pointer: fine) { .footer-top:hover { color: var(--text); } }
.footer-clock { font-family: var(--font-mono); font-size: var(--step--1); color: var(--text-secondary); }

/* ── Page intro (work/about/contact/privacy headers) ─────────────── */

.page-intro { padding-top: var(--sp-9); padding-bottom: var(--sp-8); }
/* sp-9 rather than the old sp-10 now these headings are much larger —
   the type provides the presence the extra padding used to. */
@media (min-width: 1024px) { .page-intro { padding-top: var(--sp-9); } }
.page-intro h1 { font-size: var(--step-4); font-weight: 600; letter-spacing: -0.03em; max-width: 16ch; }
.page-intro p { margin-top: var(--sp-5); color: var(--text-secondary); font-size: var(--step-1); max-width: 52ch; }
/* Every inner page opened on a heading that topped out around 80px on
   a 1312px measure — small enough that these intros read as timid next
   to the homepage. Scales up from 1024px, pitched below the hero since
   these are secondary pages and shouldn't outshout it.

   Sized against the container rather than the viewport: thanks.html
   deliberately narrows its container to 560px, and a viewport-based
   size blew the heading there out to four wrapped lines and a 1173px
   intro. cqw tracks whatever measure the heading actually has, so the
   wide pages get their large heading and the narrow one stays sane.
   If cqw isn't supported the declaration is simply invalid and the
   --step-4 rule above still applies, which is the old behaviour. */
.page-intro > .container { container-type: inline-size; }
/* Tight leading is not cosmetic here. These headings inherited the body
   line-height (~1.55), which is right for prose and badly wrong for
   110px display type — two lines came to 342px, roughly 110px of pure
   dead space, which is what pushed the last item of the services index
   below the fold. The hero sets 0.95 for the same reason. */
.page-intro h1 { line-height: 1.03; }
@media (min-width: 1024px) {
  /* Capped at --step-4's ceiling rather than 7rem. Contact's title reached
   110px, bigger than every page except the homepage, and the case-study
   titles top out at this same 5.2rem. */
  .page-intro h1 { font-size: clamp(2.75rem, 8.4cqw, 5.2rem); max-width: 18ch; }
}
.page-intro .btn { margin-top: var(--sp-7); }

/* Two-column intro foot — heading stays full width above it, so it gets
   the whole measure to break across. Sitting the heading inside the
   left column instead made it wrap to three lines in the narrower
   track and pushed the intro past 1000px tall. Only services.html
   supplies the second column, so the other .page-intro pages are
   untouched by this. */
.page-intro-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-8); margin-top: var(--sp-7); }
@media (min-width: 900px) {
  .page-intro-grid {
    grid-template-columns: minmax(0, 1fr) clamp(240px, 24vw, 320px);
    gap: var(--sp-10);
    align-items: start;
  }
}
.page-intro-grid p { margin-top: 0; }
/* Split heroes hold the label, headline and copy inside the grid, so the
   grid's top margin and the paragraph reset above both land inside the
   hero. Both unscoped from the 900px query: below it the reset put the
   paragraph 8px into the headline on Services, Work and About (0px on
   Pricing), and the margin set the eyebrow 48px lower than on every
   other page. */
.page-intro-grid.is-split { margin-top: 0; }
.page-intro-grid.is-split p { margin-top: var(--sp-5); }

/* ── Alternating section background ──────────────────────────────── */
/* Applied manually to every second plain content section per page —
   not nth-child, since .bg-ink and .cta-band already carry their own
   distinct treatment and shouldn't be pulled into the alternation.

   Solid signal blue, not a tint — every component in this codebase
   was built to read its colour from --text/--text-secondary/
   --hairline-color rather than hardcoded values specifically so a
   surface like this could re-scope those three tokens once and have
   headings, paragraphs, mono-labels and hairlines all pick up the
   right foreground automatically through normal inheritance, with no
   need to chase down every individual selector.

   Re-scoped to --on-signal/--on-signal-secondary/--on-signal-hairline
   (tokens.css), not directly to --paper — --paper flips meaning
   between themes (it's light mode's canvas, near-black in dark mode),
   but which foreground reads best on --signal doesn't track that same
   flip: dark mode's --signal is deliberately much brighter than light
   mode's (lifted for its own AA pass against the dark canvas), so it
   actually wants DARK foreground, not light. Tried the --paper
   shortcut first and it produced near-black-on-near-black text in
   dark mode — caught in verification, not before shipping. --on-signal
   is computed and verified per theme independently in tokens.css.

   The one thing that inheritance can't safely fix: anything that
   reads --text/--text-secondary while sitting on its OWN white
   surface nested inside here (form fields, the carousel cards, the
   form success card) would otherwise go white-on-white. Each of those
   re-scopes the same three tokens back to the ink primitives directly
   below. --signal itself is deliberately left alone — redefining it
   here would also recolour .bg-alt's own background, since both are
   set in the same cascade. Anything that uses --signal as a
   foreground colour on this background (hover states, link
   underlines) gets a small direct override instead. */
.bg-alt {
  background: var(--signal);
  --text: var(--on-signal);
  --text-secondary: var(--on-signal-secondary);
  /* --canvas means "the ground behind me", and on this band the ground
     is the band. Without this it stayed page-paper while --text became
     --on-signal, so anything pairing the two — a checked pill, most
     obviously — rendered paper on paper. Caught it on .bg-ink first and
     patched only that band, which left the identical trap here. */
  --canvas: var(--signal);
  --hairline-color: var(--on-signal-hairline);
  color: var(--text);
}

.bg-alt .link-underline { background-image: linear-gradient(var(--on-signal), var(--on-signal)); }
.bg-alt .email-link { color: var(--on-signal); }
/* The global focus ring is 2px solid var(--signal), which on this band
   is the background colour — every control on the contact form had an
   invisible focus indicator. Scoped here rather than changing the global
   ring, which is correct on paper and ink. */
.bg-alt :focus-visible { outline-color: var(--on-signal); }
/* Same story for the form's error text: #C0392B sits at 1.38:1 on blue.
   --on-signal-error keeps a red hue at 5.81:1 light, 4.73:1 dark. */
.bg-alt .form-status { color: var(--on-signal-error); }

@media (hover: hover) and (pointer: fine) {
  /* .service-row:hover (base rule, line ~463) turns the row's own
     background white regardless of section — a real bug here before
     this fix: .bg-alt's light --text/--text-secondary kept cascading
     onto that white hover background, rendering near-white text on
     white, almost invisible. Re-scope back to dark ink on hover, same
     as the other nested-white-surface cases, and keep the number/rule
     accent signal-blue rather than on-signal (which is tuned for
     reading on the blue section background, not this white one).
     `color` must be re-declared here too, not just the custom
     properties: .bg-alt sets `color: var(--text)` on itself, and that
     resolved value is what inherits down — redefining --text deeper in
     the tree doesn't retroactively change an already-inherited color. */
  .bg-alt .service-row:hover {
    --text: var(--ink-900);
    --text-secondary: var(--ink-500);
    color: var(--text);
  }
  .bg-alt .service-row:hover .desc,
  .bg-alt .service-row:hover .chip { color: var(--text-secondary); }
  .bg-alt .service-row:hover .num { color: var(--signal); }
  .bg-alt .service-row:hover .rule { background: var(--signal); }
}

/* Nested white surfaces re-scope back to the normal ink primitives —
   see the long comment above for why this can't just be inheritance. */
.bg-alt .field input,
.bg-alt .field select,
.bg-alt .field textarea,
.bg-alt .work-card-lg,
.bg-alt .work-card,
.bg-alt .form-success,
.bg-alt .work-carousel-next {
  --text: var(--ink-900);
  --text-secondary: var(--ink-500);
  --hairline-color: var(--ink-300);
}
.bg-alt .work-card { background: var(--surface); }

/* --signal is tuned against the page canvas, not against a card sitting
   on the blue band. On the dark-mode card surface (#1B1B1E) it measures
   4.34:1 — under the 4.5 floor, on five labels on the homepage.
   --signal-hover is the same hue one step further from that surface:
   5.92:1 in dark, and darker again in light, so the label stays blue and
   becomes legible in both. */
.bg-alt .work-card-lg .mono-label { color: var(--signal-hover); }

/* ── Dark section (case-study hero, stat bands) ──────────────────── */

/* Deliberately theme-invariant: this is an always-dark editorial
   contrast band (case-study hero, stat figures), not a surface that
   should flip with the light/dark toggle — --ink-900 itself swaps
   meaning between themes (near-black in light mode, near-white in
   dark), so using it here would invert the band under dark mode. */
.bg-ink {
  background: #0B0B0C;
  color: #F7F5F1;
  /* Re-scope the semantic tokens, same approach as .bg-alt, so any
     component reading --text/--text-secondary/--hairline-color renders
     correctly in here without needing its own override. Hardcoded
     rather than aliased to --ink-900/--paper for the theme-invariance
     reason above: those flip meaning between themes, this band never
     does. Verified on #0B0B0C — 18.1:1 for --text, 9.0:1 for
     --text-secondary; the hairline value is 1.7:1, in line with the
     1.9:1 --ink-300 hairline used on light surfaces (hairlines only,
     never text). */
  --text: #F7F5F1;
  --text-secondary: rgba(247, 245, 241, 0.7);
  /* Same reasoning as .bg-alt, hardcoded like the rest of this block. */
  --canvas: #0B0B0C;
  --hairline-color: rgba(247, 245, 241, 0.2);
}
/* base.css sets a direct h1-h6 color rule, and a direct rule always
   wins over inheritance regardless of specificity — so headings inside
   .bg-ink need an explicit override or they render in the page's
   default (near-black) text colour, invisible against this same dark
   background. Caught in verification, not before shipping. */
.bg-ink h1, .bg-ink h2, .bg-ink h3, .bg-ink h4 { color: inherit; }
.bg-ink .text-secondary, .bg-ink .mono-label { color: rgba(247, 245, 241, 0.65); }

/* ── Case study meta + narrative ──────────────────────────────────── */

/* Bottom padding restored. The original case study hero ended on a
   full-bleed .cs-hero-shot whose top corners met the band edge, so the
   band deliberately had none — these heroes end on the lead paragraph
   and the live-site link, which were sitting flush against the edge.
   The old shot layout keeps its flush treatment below. */
.cs-hero { padding: var(--sp-9) 0; }
.cs-hero:has(.cs-hero-shot) { padding-bottom: 0; }
@media (min-width: 1024px) { .cs-hero { padding-top: var(--sp-10); } }
.cs-meta { display: flex; flex-wrap: wrap; gap: var(--sp-5) var(--sp-8); margin-bottom: var(--sp-7); }
/* Was color-mix(--paper 60%) — but --paper is the theme-aware canvas
   token (light in light mode, near-black in dark), and this label only
   ever sits on the always-dark .bg-ink band, so in dark mode it
   rendered near-black on near-black. --text-secondary is re-scoped by
   .bg-ink itself to a theme-invariant light value, which is what this
   actually wanted. */
.cs-meta .label { font-family: var(--font-mono); font-size: var(--step--1); letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); display: block; }
.cs-meta .value { font-size: var(--step--1); margin-top: var(--sp-1); display: block; }
.cs-hero h1 { font-size: var(--step-4); font-weight: 600; letter-spacing: -0.03em; max-width: 16ch; margin-bottom: var(--sp-8); }
/* 16/9, not 21/9. These are device mockups — the site composited onto a
   monitor in a photographed room — so the subject sits in the middle of
   the frame with the desk below and the ceiling above. A 21/9 crop cut
   through the monitor itself. */
.cs-hero-shot { aspect-ratio: 16 / 9; overflow: hidden; border-radius: var(--radius-md) var(--radius-md) 0 0; margin: 0; }
.cs-hero-shot img { object-position: center; }
.cs-hero-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 2/1 from 1024px. At 16/9 across the full measure these heroes ran
   1,449-1,487px and the first chapter started two screens down. The
   mockups have desk and ceiling to spare above and below the monitor. */
@media (min-width: 1024px) { .cs-hero-shot { aspect-ratio: 2 / 1; } }

.cs-narrative { max-width: 720px; margin: 0 auto; padding-block: var(--sp-10); }
.cs-block + .cs-block { margin-top: var(--sp-9); }
.cs-block .mono-label { margin-bottom: var(--sp-4); }
.cs-block h2 { font-size: var(--step-2); font-weight: 500; margin-bottom: var(--sp-4); }
.cs-block p { color: var(--text-secondary); }
.cs-block p + p { margin-top: var(--sp-4); }

.device-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-6); align-items: end; padding-block: var(--sp-9); }
@media (min-width: 768px) { .device-grid { grid-template-columns: minmax(0,8fr) minmax(0,4fr); } }
.device-desktop { background: var(--paper-2); border-radius: var(--radius-md); overflow: hidden; aspect-ratio: 16/10; }
.device-desktop img { width: 100%; height: 100%; object-fit: cover; display: block; }
.device-phone {
  background: var(--paper-2);
  border: var(--hairline);
  border-radius: 28px;
  overflow: hidden;
  padding: var(--sp-3);
  max-width: 260px;
  justify-self: center;
}
.device-phone .shot { border-radius: 18px; overflow: hidden; aspect-ratio: 9/16; }
.device-phone img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ── Team ─────────────────────────────────────────────────────────── */
/* Real headshot, replacing the monogram that stood in while there was
   no photo. The source is 512px square, so display sizes are capped
   near that: the homepage thumbnail at 120px and the about portrait at
   300px both stay at or under 1:1 on a 2x display rather than being
   upscaled into softness. */
.team-member {
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
  margin-top: var(--sp-8);
}
@media (min-width: 768px) { .team-member { flex-direction: row; align-items: flex-start; gap: var(--sp-8); } }

.team-portrait {
  flex-shrink: 0;
  width: 120px;
  height: 120px;
  margin: 0;
  border-radius: 999px;
  overflow: hidden;
  background: var(--paper-2);
}
.team-portrait img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* About page portrait. Lives in the intro grid's second column, which
   is what fills the dead space the lead paragraph used to sit beside. */
.founder-portrait {
  margin: 0;
  /* Circular: aspect-ratio 1 above makes the box square, so a large
     radius resolves to a true circle rather than a squashed oval. */
  border-radius: 999px;
  overflow: hidden;
  background: var(--paper-2);
  max-width: 300px;
  aspect-ratio: 1;
}
.founder-portrait img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Stack list: hairline rows rather than another card grid — the page
   already leans on cards elsewhere and three grid sections in a row was
   most of why it read as repetitive. */
.stack-list { margin: var(--sp-7) 0 0; border-top: var(--hairline); }
.stack-list-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-2);
  padding: var(--sp-5) 0;
  border-bottom: var(--hairline);
}
@media (min-width: 768px) {
  .stack-list-row { grid-template-columns: minmax(0, 2fr) minmax(0, 7fr); gap: var(--sp-6); align-items: baseline; }
}
.stack-list dt {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 500;
  letter-spacing: -0.01em;
}
.stack-list dd { margin: 0; color: var(--text-secondary); max-width: 62ch; }

.team-info h2 { font-size: var(--step-2); font-weight: 500; letter-spacing: -0.02em; }
.team-info .mono-label { margin: var(--sp-2) 0 var(--sp-4); }
.team-info p { color: var(--text-secondary); max-width: 56ch; }
/* base.css zeroes margins on p, so consecutive paragraphs in the bio
   ran together with no gap at all. */
.team-info p + p { margin-top: var(--sp-4); }
.team-info .link-arrow { margin-top: var(--sp-4); }

/* ── Testimonial ──────────────────────────────────────────────────── */
/* CURRENTLY UNUSED. The three quotes on the homepage were placeholder
   copy — they credited "Webs Design 3W", a name that appears nowhere
   else, and named clients who appear nowhere in the Work section — so
   the section was removed rather than left standing on a page asking for
   five figures. The component is kept ready for real quotes: drop a
   .testimonial-grid of .testimonial-card back into index.html.

   .testimonial-card alone (no .testimonial-grid ancestor) is a single
   large centred pull-quote, for whenever there's just one to feature.
   Nested in .testimonial-grid it's a smaller card in a 3-up row — the
   quotes are full paragraphs, not one-liners, so the standalone
   step-3 size would be unreadable three-wide. */
.testimonial-card { max-width: 640px; margin: 0 auto; padding-block: var(--sp-9); text-align: left; }
.testimonial-quote { font-family: var(--font-serif); font-size: var(--step-3); line-height: 1.3; color: var(--text); }
.testimonial-attribution { margin-top: var(--sp-5); font-size: var(--step--1); color: var(--text-secondary); }
.testimonial-attribution strong { color: var(--text); font-weight: 500; }

/* Matches the step-3 heading size every other section sets for itself
   (#work h2, .faq-intro h2, .process-intro h2) — base.css deliberately
   sets no font-size on headings, so an unstyled h2 here would fall
   back to the UA default and read visibly smaller than its peers. */
.testimonials-heading { font-size: var(--step-3); font-weight: 500; letter-spacing: -0.02em; }

.testimonial-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
  margin-top: var(--sp-7);
}
@media (min-width: 900px) { .testimonial-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.testimonial-grid .testimonial-card {
  max-width: none;
  margin: 0;
  padding: var(--sp-7);
  background: var(--paper-2);
  border-radius: var(--radius-md);
}
/* Instrument Serif is a display face — distinctive at the standalone
   card's large single-line size above, but it reads oddly once shrunk
   down and run as a dense paragraph three-wide. Body font instead. */
.testimonial-grid .testimonial-quote { font-family: var(--font-body); font-size: var(--step-0); line-height: 1.5; }
.testimonial-grid .testimonial-attribution { margin-top: var(--sp-4); }

/* ── Service detail list (services.html) ──────────────────────────── */
/* Expanded sibling of .service-row on the homepage — same spine/
   hairline language, but one column of copy instead of a 4-up grid,
   since each entry here carries a full paragraph and a tag list
   rather than a one-line description. */
/* Eight services ran as one narrow column down the middle-left of a
   1425px band, each row 383px tall for 254px of content, with the right
   half of every band empty. Two columns from 900px: the page uses its
   width, and it is roughly half as long to get through. */
.service-detail-list { border-top: var(--hairline); }
.service-detail {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-3);
  padding-block: var(--sp-6);
  border-bottom: var(--hairline);
}
@media (min-width: 768px) {
  .service-detail { grid-template-columns: 48px minmax(0, 1fr); gap: var(--sp-5); padding-block: var(--sp-7); }
}

/* Rows, not cards. This was two columns, and both groups of three
   orphaned their third service beside a full empty cell — 235,504px² in
   "Design & build", 216,480px² again in "Host, secure, maintain". A row
   cannot orphan anything and it uses the whole measure instead of half.
   The body div goes display:contents so its children become grid items
   of the row itself; every child is placed explicitly, because in source
   order they would otherwise fill columns 2,3,4,5.

   Two row shapes, because the pill is expensive. Giving it a column of
   its own costs 212px plus a gap, and the first attempt did that from
   1100px — where it left the description 188px, about 21 characters a
   line. Below 1280px the pill sits under the title instead, and the
   description keeps a readable measure. */
@media (min-width: 1000px) {
  .service-detail {
    grid-template-columns: 48px minmax(0, 300px) minmax(0, 1fr);
    align-items: start;
    column-gap: var(--sp-6);
    row-gap: var(--sp-4);
    padding-block: var(--sp-6);
  }
  .service-detail-body { display: contents; }
  .service-detail .num { grid-column: 1; grid-row: 1; }
  .service-detail-body h3 { grid-column: 2; grid-row: 1; margin-bottom: 0; }
  .service-detail-body p { grid-column: 3; grid-row: 1; }
  .service-detail-tags { grid-column: 3; grid-row: 2; margin-top: 0; }
  .service-pick { grid-column: 2; grid-row: 2; margin-top: 0; }
}

/* Wide enough for the pill to have its own column and still leave the
   description ~50 characters. */
@media (min-width: 1280px) {
  .service-detail {
    /* Pinned, not max-content. "ADDED TO BRIEF" is 21px wider than "ADD
       TO BRIEF", so a content-sized column shrank the paragraph beside
       it by 21px the moment anyone picked a service — the text reflowed
       under the cursor as a reward for clicking. */
    grid-template-columns: 48px minmax(0, 340px) minmax(0, 1fr) minmax(212px, max-content);
  }
  .service-detail-body h3 { grid-row: 1 / span 2; }
  .service-pick { grid-column: 4; grid-row: 1; justify-self: end; }
  .service-pick-face { min-width: 212px; justify-content: center; }
}

@media (min-width: 1440px) {
  .service-detail { column-gap: var(--sp-7); }
}

.service-detail .num { font-family: var(--font-mono); font-size: var(--step--1); color: var(--text-secondary); }
.service-detail-body h3 { font-size: var(--step-2); font-weight: 500; letter-spacing: -0.02em; margin-bottom: var(--sp-3); }
.service-detail-body p { color: var(--text-secondary); }
.service-detail-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-secondary);
}

/* ── Hero art panel (services.html, work.html) ────────────────────── */
/* The hero's left column ran 246px of content down an 864px-wide, 412px-
   tall cell: 167px of dead height under the button and ~294px of unused
   width beside a paragraph capped at 52ch. What fills it is generated,
   not fetched — a parametric surface rasterised to glyphs every frame,
   so it costs a few KB of JavaScript and no image request at all.

   The panel is deliberately dark in both themes, like .bg-ink: the piece
   is built out of light on black, and inverting it in light mode would
   turn a glow into soot. */
/* On a .bg-ink band the panel's ground is the band's ground, so the
   border is the only thing separating them. */
.bg-ink .artpanel { border-color: rgba(255, 255, 255, 0.22); }

.artpanel {
  margin: 0;
  border-radius: 12px;
  overflow: hidden;
  background: #08080A;
  border: 1px solid rgba(255, 255, 255, 0.12);
}
.artpanel-canvas {
  display: block;
  width: 100%;
  /* Square. These shapes are round things: in an 862x320 panel one could only
     ever fill a 320px circle and leave two thirds of the black empty. */
  aspect-ratio: 1 / 1;
  background: #08080A;
}
.artpanel-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  font-family: var(--font-mono);
  /* Was 0.72rem, which rendered at 11.5px — under the size small print
     stays comfortable at, on all three panels. */
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* Fixed, not tokenised: this bar sits on its own black ground in both
     themes, so --text-secondary would flip out from under it. 7.2:1. */
  color: rgba(237, 235, 231, 0.72);
}
.artpanel-note { text-align: right; }

/* Stacked on a phone it must not grow into a huge square. */
.artpanel { max-width: 420px; }
@media (max-width: 599px) {
  .artpanel-note { display: none; }
}

/* Two halves: the copy, and the art beside it, both starting on the
   same line. Centred, a 250px block of copy sat 183px below the top of a
   651px panel — beside it in the markup, nowhere near it on the page.

   Keyed off .is-split rather than a page class, so any hero that carries
   an art panel gets the split without a new rule each time. */
@media (min-width: 900px) {
  .page-intro-grid.is-split {
    grid-template-columns: 1fr 1fr;
    column-gap: var(--sp-9);
    align-items: start;
  }
  .is-split .page-intro-lead {
    grid-column: 1;
    /* The headline lives in this half now, so it has to size against
       this column rather than the full container — 8.4cqw of 1312px is
       110px, which is a headline for a full-width row, not a half. */
    container-type: inline-size;
  }
  .is-split h1 {
    font-size: clamp(2.25rem, 10cqw, 3.9rem);
    max-width: 15ch;
  }
  .is-split .artpanel {
    grid-column: 2;
    justify-self: end;
    width: 100%;
    /* Uncapped it reached 608px square in a half-column, which made the
       hero 1,141px tall and pushed the CTA under the fold on a 1440x900
       screen. */
    max-width: 520px;
  }

  /* About puts a photograph in the art slot. 300px is right when it is a
     small aside; in half a hero it has to hold its side. */
  .is-split .founder-portrait {
    grid-column: 2;
    justify-self: end;
    width: 100%;
    max-width: 420px;
  }

  /* Pricing's heading sits on `.config-hero .config-heading`, which ties
     `.is-split .config-heading` on specificity and wins on source order —
     it is 800 lines further down the file. Two classes on the grid breaks
     the tie without depending on where this rule sits. */
  .page-intro-grid.is-split .config-heading {
    font-size: clamp(2.25rem, 10cqw, 3.9rem);
    max-width: 15ch;
  }
}

/* ── Current-page CTA ─────────────────────────────────────────────── */
/* On Contact, the header's "Get a quote" points at the page you are on.
   It keeps aria-current for assistive tech and steps back to an outline,
   so it stops asking you to go where you already are. */
.site-header .btn[aria-current="page"] { background: transparent; color: var(--text); border-color: var(--hairline-color); }
@media (hover: hover) and (pointer: fine) {
  .site-header .btn[aria-current="page"]:hover { background: transparent; color: var(--text); border-color: var(--text); }
}

/* Homepage closing band carries the page's last ask. */
.statement .btn { margin-top: var(--sp-8); }

/* Pricing hero: points at the builder, which sits 2,620px down on a phone. */
.config-jump { margin-top: var(--sp-6); }
@media (hover: hover) and (pointer: fine) { .config-jump:hover .arrow { transform: translateY(3px); } }

/* ── Touch target sizing ──────────────────────────────────────────── */
/* These all clear WCAG 2.2 AA's 24px floor already. This lifts them to
   the 44px a finger actually wants, and only where there is a finger:
   scoped to coarse pointers so the desktop layout keeps its density and
   nothing moves for mouse users. Measured at 375px before: 30 targets
   under 44px on the homepage, 21 of them in the footer at 25px.
   Padding rather than height, so no box changes size on screen. */
@media (pointer: coarse) {
  .footer-col a { padding-block: 12px; }
  .link-arrow { min-height: 44px; }
  .wordmark { display: inline-flex; align-items: center; min-height: 44px; }
  .hero-chip { min-height: 44px; }
  .footer-bottom a { display: inline-flex; align-items: center; min-height: 44px; }
  .theme-toggle { min-height: 44px; min-width: 44px; }
  .footer-mark { display: inline-flex; align-items: center; min-height: 44px; }
  .footer-email { display: inline-flex; align-items: center; min-height: 44px; }
  .email-link { display: inline-flex; align-items: center; min-height: 44px; }
  .feature-why { min-height: 44px; }
  .cs-more-all { display: inline-flex; align-items: center; min-height: 44px; }
  /* The work list's project titles are the link, inside an h3. */
  .work-card-body h3 a { display: inline-flex; align-items: center; min-height: 44px; }
}

/* ── 404 ──────────────────────────────────────────────────────────── */
/* GitHub Pages served its own generic 404 before this: no nav, no
   footer, no way back. Uses the standard button components so every
   target is a full 44px. */
.not-found-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-7);
}

/* ── Services page rhythm ─────────────────────────────────────────── */
/* --sp-11 top and bottom is the site's section padding, and it is right
   on pages where a band holds one big idea. This page has four bands
   holding a heading and three rows each: 384px of padding wrapped around
   roughly 700px of content, four times over, was 1,536px of the page —
   a quarter of its whole length — spent on nothing. Scoped to this page
   so the rest of the site keeps its rhythm. */
/* Only from 1024px, which is where .section actually takes --sp-11.
   Below that the site already uses --sp-9 (96px) and this override,
   left unscoped, was pushing mobile padding UP from 96px to 128px. */
@media (min-width: 1024px) {
  .services-page .section { padding-block: var(--sp-10); }

  /* An empty state does not need 384px of room to say "nothing yet". It
     grows on its own once services are picked, because the summary line
     fills with their names. */
  .services-page .section.bg-ink { padding-block: var(--sp-9); }
}

/* Eight rows that do nothing on hover read as a table. The tint bleeds
   past the text on both sides so the whole row lights up, while the
   negative inline margin keeps every column exactly where it was. */
@media (hover: hover) and (min-width: 1100px) {
  .service-detail {
    padding-inline: var(--sp-4);
    margin-inline: calc(var(--sp-4) * -1);
    transition: background var(--dur-fast) var(--ease-out);
  }
  .service-detail:hover { background: color-mix(in srgb, var(--text) 7%, transparent); }
  .service-detail:hover .service-pick-face { border-color: currentColor; }
}

/* ── Service groups + brief builder (services.html) ───────────────── */
/* "Design & build" and "Website Design" both measured 36.72px — a group
   heading and one of the things inside it, identical. --step-4 fixed that
   but overshot: 80px groups under a 61px page title. --step-3 sits between
   the two, so title > group > service reads in order. */
.service-group-heading {
  font-size: var(--step-3);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
  margin-bottom: var(--sp-7);
  max-width: 18ch;
}

/* "Add to brief" toggle. The native checkbox stays in the layout and
   focusable (clipped, not display:none) so keyboard and screen-reader
   users get the real control; .service-pick-face is the visible pill. */
.service-pick {
  display: inline-flex;
  margin-top: var(--sp-5);
  cursor: pointer;
}
.service-pick input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
  pointer-events: none;
}
.service-pick-face {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  /* 36px on the one control this page is built around, while every
     other button on the site is 44px. */
  min-height: 44px;
  padding: 0 var(--sp-5);
  border: var(--hairline);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text);
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}
.service-pick-face::before {
  content: '+';
  font-size: 1.1em;
  line-height: 1;
}
.service-pick .pick-added { display: none; }
.service-pick input:checked ~ .service-pick-face .pick-add { display: none; }
.service-pick input:checked ~ .service-pick-face .pick-added { display: inline; }
.service-pick input:checked ~ .service-pick-face::before { content: '✓'; }

/* Checked, on a paper section: fill with the brand blue. --on-signal is
   the token already computed per theme for text sitting on --signal. */
.service-pick input:checked ~ .service-pick-face {
  background: var(--signal);
  border-color: var(--signal);
  color: var(--on-signal);
}
/* Checked, on a blue section: --signal would be blue-on-blue, so the
   fill and text swap round — light pill, blue text. Both sides of that
   pairing are the same two tokens, just reversed. */
.bg-alt .service-pick input:checked ~ .service-pick-face {
  background: var(--on-signal);
  border-color: var(--on-signal);
  color: var(--signal);
}
/* Keyboard focus has to be visible on the pill, since the real input
   it belongs to is clipped out of sight. */
.service-pick input:focus-visible ~ .service-pick-face {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}
@media (hover: hover) and (pointer: fine) {
  .service-pick:hover .service-pick-face { border-color: currentColor; }
}

.brief-builder {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  align-items: flex-start;
  padding: var(--sp-7);
  border: var(--hairline);
  border-radius: var(--radius-md);
  background: var(--paper-2);
}
@media (min-width: 768px) {
  .brief-builder { flex-direction: row; align-items: center; justify-content: space-between; gap: var(--sp-8); padding: var(--sp-8); }
  .brief-builder-body { flex: 1 1 auto; }
}
/* On the ink band the --paper-2 fill would be a light card carrying
   .bg-ink's light text — the nested-surface trap. Outlined panel
   instead: transparent, with the band's own re-scoped hairline. */
.bg-ink .brief-builder { background: transparent; }

/* The tally, at the scale the thing deserves. Ticking eight services
   used to produce one line of muted grey in a 542px band holding 60px
   of content. */
.brief-count {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  margin-top: var(--sp-3);
  color: var(--text);
}
.brief-summary {
  margin: var(--sp-4) 0 0;
  color: var(--text-secondary);
  max-width: 60ch;
}
.brief-builder .btn { flex-shrink: 0; }

/* ── Case study: editorial chapters ───────────────────────────────── */
/* Replaces a single column of four identically shaped text blocks —
   same label, same measure, same alignment, stacked in the left half of
   the page. Each chapter is now its own band with a sticky label in a
   narrow left column and the prose in a wide right one, so the page
   uses its full width and each section is visually distinct from the
   last rather than a repeat of it. */
.cs-lead {
  margin-top: var(--sp-6);
  font-size: var(--step-1);
  max-width: 46ch;
  color: inherit;
  opacity: 0.85;
}

.cs-row { display: grid; grid-template-columns: 1fr; gap: var(--sp-5); }
@media (min-width: 900px) {
  .cs-row { grid-template-columns: minmax(0, 3fr) minmax(0, 8fr); gap: var(--sp-9); }
  /* Label tracks the prose while a long chapter scrolls past. Needs
     align-self:start or the grid stretches the cell and sticky has no
     room to move within it. */
  .cs-row-label { position: sticky; top: 96px; align-self: start; }
}
.cs-row-body p { max-width: 62ch; color: var(--text-secondary); }
.cs-row-body p + p { margin-top: var(--sp-5); }
.cs-row-label .num { margin-right: var(--sp-2); }

/* Pull quote — the one place per chapter where the type steps up, so
   the eye has somewhere to land in a long run of body copy. */
.cs-pull {
  margin-top: var(--sp-7) !important;
  padding-top: var(--sp-6);
  border-top: var(--hairline);
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.2;
  color: var(--text) !important;
  max-width: 26ch !important;
}

/* ── Case study: component blocks ─────────────────────────────────── */
/* A case study reads as a case study because of components — swatches,
   decision cards, an outcomes table — not paragraphs. These are the
   pieces that carry the visual weight on these pages. */
/* Live-site link in the case study hero. Both colours are hardcoded
   because this sits on the always-dark .bg-ink band: --on-signal flips
   to near-black in dark mode, which would drop blue-on-pill from 6.9:1
   to 2.6:1. Fixed values hold 6.9:1 in both themes. */
.cs-live-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-7);
  padding: 0 var(--sp-6);
  height: 46px;
  border-radius: 999px;
  background: #F7F5F1;
  color: #1B34F0;
  font-weight: 500;
  font-size: var(--step--1);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-out), gap var(--dur-fast) var(--ease-out);
}
.cs-live-domain {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  opacity: 0.6;
  padding-left: var(--sp-3);
  border-left: 1px solid currentColor;
}
.cs-live-link .arrow { transition: transform var(--dur-base) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .cs-live-link:hover { transform: translateY(-2px); }
  .cs-live-link:hover .arrow { transform: translateX(3px); }
}
@media (max-width: 480px) {
  .cs-live-link { height: auto; padding: var(--sp-3) var(--sp-5); flex-wrap: wrap; }
  .cs-live-domain { border-left: 0; padding-left: 0; width: 100%; }
}

.cs-section-intro { margin-top: var(--sp-4); max-width: 60ch; color: var(--text-secondary); }

/* Education list on the about page. */
.about-credentials { margin-top: var(--sp-9); padding-top: var(--sp-6); border-top: var(--hairline); }
.about-credentials ul {
  list-style: none;
  margin: var(--sp-4) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-3);
  color: var(--text-secondary);
}
@media (min-width: 768px) { .about-credentials ul { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-6); } }
.about-credentials strong { display: block; color: var(--text); font-weight: 500; }
.cs-cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4);
  margin-top: var(--sp-7);
}
@media (min-width: 700px) { .cs-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .cs-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.cs-card {
  padding: var(--sp-6);
  border: var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.bg-alt .cs-card,
.bg-ink .cs-card { background: transparent; }
.cs-card h3 {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 500;
  padding-bottom: var(--sp-3);
  margin-bottom: var(--sp-3);
  border-bottom: var(--hairline);
}
.cs-card p { color: var(--text-secondary); font-size: var(--step--1); margin: 0; max-width: none; }

/* Palette — the swatches are the point, so they lead and the labels
   sit under them rather than beside. */
.cs-palette {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--sp-4);
  margin-top: var(--sp-7);
}
.cs-swatch-chip {
  height: 84px;
  border-radius: var(--radius-md);
  border: 1px solid rgba(128, 128, 128, 0.25);
}
.cs-swatch-name { margin-top: var(--sp-3); font-weight: 500; font-size: var(--step--1); }
.cs-swatch-role { font-size: var(--step--1); color: var(--text-secondary); }

/* Outcomes table — deliverable against what actually changed. */
.cs-table { width: 100%; border-collapse: collapse; margin-top: var(--sp-7); font-size: var(--step--1); }
.cs-table th {
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 500;
  padding: 0 var(--sp-5) var(--sp-3) 0;
  border-bottom: 1px solid currentColor;
}
.cs-table td { padding: var(--sp-4) var(--sp-5) var(--sp-4) 0; border-bottom: var(--hairline); vertical-align: top; color: var(--text-secondary); }
.cs-table td:first-child { color: var(--text); font-weight: 500; width: 34%; }
@media (max-width: 600px) {
  .cs-table, .cs-table tbody, .cs-table tr, .cs-table td { display: block; width: 100%; }
  .cs-table thead { display: none; }
  .cs-table tr { padding-block: var(--sp-4); border-bottom: var(--hairline); }
  .cs-table td { border: 0; padding: 0; }
  .cs-table td:first-child { width: auto; margin-bottom: var(--sp-2); }
}

/* ── Case study: before/after compare ─────────────────────────────── */
/* Full-page captures are extremely tall (2560px), so each sits in its
   own scroll pane rather than being scaled down to an illegible strip.
   The two source images have different native widths, so the panes are
   fixed-height and the images fill the width — a drag-wipe comparison
   would need matching aspect ratios, which these don't have. */
/* Panes are capped to the narrower capture's natural width (319px) and
   the pair is centred, rather than each filling a half-width column.
   Letting them fill left the low-res "after" as a narrow strip adrift
   in an empty pane beside a "before" that filled its own — the two read
   as a broken layout rather than a comparison. Matched widths make both
   images sharp (one scaled down, one near 1:1) and the pairing
   deliberate. */
/* Breaks the container so the comparison runs near full-bleed — this is
   the only real imagery on the page and it was previously a pair of
   320px thumbnails lost in a wide column. */
.cs-compare-wrap { margin-top: var(--sp-8); }
@media (min-width: 900px) {
  .cs-compare-wrap {
    width: calc(100vw - var(--page-pad) * 2);
    max-width: var(--content-max);
    margin-left: calc(50% - min(100vw - var(--page-pad) * 2, var(--content-max)) / 2);
  }
}
.cs-compare-note { margin-top: var(--sp-4); color: var(--text-secondary); }
.cs-compare {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
  margin-top: var(--sp-5);
}
@media (min-width: 768px) { .cs-compare { grid-template-columns: 1fr 1fr; gap: var(--sp-6); } }
.cs-compare figure { margin: 0; }
.cs-compare figcaption { margin-bottom: var(--sp-3); }
.cs-compare-scroll {
  height: 70vh;
  min-height: 480px;
  max-width: 520px;
  margin: 0 auto;
  overflow-y: auto;
  border: var(--hairline);
  border-radius: var(--radius-md);
  background: var(--paper-2);
  overscroll-behavior: contain; /* don't hijack the page scroll at the ends */
}
/* Safe to fill the pane now it's capped at 320px: the wide capture
   scales down and the narrow one sits within a percent of 1:1, so
   neither is meaningfully upscaled. */
.cs-compare-scroll img { display: block; width: 100%; height: auto; }

.cs-deliverables {
  list-style: none;
  margin: var(--sp-4) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-3);
  color: var(--text-secondary);
}
@media (min-width: 768px) { .cs-deliverables { grid-template-columns: 1fr 1fr; gap: var(--sp-3) var(--sp-7); } }
.cs-deliverables li { display: flex; align-items: baseline; gap: var(--sp-3); }
/* .tick is --signal, which is a dark blue in light mode and all but
   invisible on this always-dark band (2.6:1). Same hardcoded lifted
   value the marquee separator uses, for the same reason. */
.bg-ink .tick { background: #5C72FF; }
/* Same trap on the blue band, but total rather than partial: .tick is
   --signal and .bg-alt IS --signal, so the marker was the exact colour
   of the surface behind it. --on-signal is the band's own foreground,
   so it tracks the theme flip with everything else on there. */
.bg-alt .tick { background: var(--on-signal); }

/* ── Featured project band ────────────────────────────────────────── */
.work-featured-inner { display: grid; grid-template-columns: 1fr; gap: var(--sp-5); }
@media (min-width: 900px) {
  .work-featured-inner { grid-template-columns: auto minmax(0, 1fr); gap: var(--sp-8); align-items: start; }
}
.work-featured-num {
  font-family: var(--font-mono);
  /* vw, not cqw: only .page-intro's container is a query container, so
     cqw here would be an invalid length and drop the declaration. */
  font-size: clamp(3rem, 9vw, 7rem);
  font-weight: 500;
  line-height: 1;
  color: currentColor;
  opacity: 0.35;
}
.work-featured-body h2 {
  line-height: 1.02;
  margin-top: var(--sp-3);
  font-size: var(--step-3);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.05;
}
.work-featured-lead { margin-top: var(--sp-5); font-size: var(--step-1); }
.work-featured-body p { color: var(--text-secondary); max-width: 60ch; }
.work-featured-body p + p { margin-top: var(--sp-4); }

/* ── Work index grid ──────────────────────────────────────────────── */
/* Two columns rather than three: at three, the four remaining cards
   broke 3+1 and left a conspicuous hole in the second row. */
.work-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-5); }
/* Deliberately not a 2x2. Four identically sized cards give the eye
   nothing to land on first and read as a file listing rather than a
   portfolio — every project claiming exactly equal weight is itself an
   editorial decision, and the wrong one. A six-column grid with 4/2 and
   2/4 spans makes each row lead with a different project and puts a
   diagonal through the block, so the page has somewhere to travel. */
@media (min-width: 700px) { .work-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* The asymmetry only from 980px. A 2-of-6 span on a tablet is about
   230px of card, which is narrower than the copy inside it wants; the
   even pair is the better layout at that width. */
@media (min-width: 980px) {
  .work-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .work-grid > *:nth-child(1) { grid-column: span 4; }
  .work-grid > *:nth-child(2) { grid-column: span 2; }
  .work-grid > *:nth-child(3) { grid-column: span 2; }
  .work-grid > *:nth-child(4) { grid-column: span 4; }
}
.work-card-num {
  font-family: var(--font-mono);
  font-size: var(--step-1);
  color: var(--text-secondary);
  padding: var(--sp-6) var(--sp-6) 0;
}

/* These cards carry no screenshot — there are no real images of the
   builds to show yet, and a stand-in would misrepresent the work. The
   copy and the platform/scope tags do the job instead, so the card
   sizes to its text rather than reserving an empty image well. */
.work-card-lg-text { padding-bottom: var(--sp-6); }
.work-card-lg .work-card-tags,
.work-card .work-card-tags { margin-top: var(--sp-4); }

/* ── Work index card ──────────────────────────────────────────────── */

.work-card { display: block; border: var(--hairline); border-radius: var(--radius-md); overflow: hidden; transition: border-color var(--dur-base) var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .work-card:hover { border-color: var(--signal); } }

/* Stretched link. Only the title was clickable — about 5% of a card
   that is roughly 225,000px2 — while the equivalent card on the
   homepage has always been a whole <a>, so the two pages behaved
   differently on the page whose entire job is getting people into a
   case study.

   The overlay goes on the heading's own anchor rather than wrapping the
   card in one, so the accessible name stays "Platinum Pilates" instead
   of becoming the entire card's text. The known cost is that prose
   inside the card can no longer be selected; for a card that exists to
   be clicked, that is the right trade. */
.work-card { position: relative; }
.work-card h3 a::after { content: ""; position: absolute; inset: 0; z-index: 1; }
@media (hover: hover) and (pointer: fine) { .work-card { cursor: pointer; } }

/* Something to tell you the card is a target at all. Sits after the
   tags and slides on hover, matching .link-arrow elsewhere. */
.work-card-go {
  display: block;
  margin-top: var(--sp-5);
  font-size: var(--step-1);
  line-height: 1;
  color: var(--signal);
  transition: transform var(--dur-base) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .work-card:hover .work-card-go { transform: translateX(6px); }
}
/* These cards are white on the blue band, so a --signal border reads as
   the background colour creeping onto the card edge. Uses the on-blue
   token instead, same swap as the service pick pills. Transform is
   declared here so the JS tilt has something to ease from. */
.work-card { transition: border-color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .bg-alt .work-card:hover { border-color: var(--on-signal); } }
.work-card-shot { aspect-ratio: 21/9; overflow: hidden; background: var(--paper-2); }
.work-card-shot img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform var(--dur-base) var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .work-card:hover .work-card-shot img { transform: scale(1.03); } }
.work-card-body { padding: var(--sp-6); }
.work-card-body h3 { font-size: var(--step-2); font-weight: 500; margin-bottom: var(--sp-2); }
.work-card-body p { color: var(--text-secondary); margin-bottom: var(--sp-4); }
.work-card-tags { display: flex; gap: var(--sp-3); font-family: var(--font-mono); font-size: var(--step--1); color: var(--text-secondary); }

/* ── Stat band (dark) ─────────────────────────────────────────────── */

.stat-band { display: flex; flex-direction: column; gap: var(--sp-8); padding-block: var(--sp-9); text-align: left; }
@media (min-width: 768px) { .stat-band { flex-direction: row; text-align: center; } }
.stat-band .figure { flex: 1; }
.stat-band .num { font-family: var(--font-display); font-weight: 600; font-size: var(--step-4); letter-spacing: -0.03em; }
.stat-band .num.accent { color: var(--signal); }
.stat-band .unit { margin-top: var(--sp-3); }

/* ── About: the route ─────────────────────────────────────────────── */
/* The three-discipline story used to be three prose paragraphs, which
   buried the only genuinely distinctive thing about it: that it is a
   sequence. Set as a timeline — a rule across the top of each step with
   a node sitting on it — so the progression reads before a word does,
   and so this section has a different shape from the hairline rows
   further down the page. The node takes currentColor, so it follows
   whichever band the section is placed on without needing an override. */
.route {
  list-style: none;
  margin: var(--sp-8) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-7);
}
@media (min-width: 860px) {
  .route { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-8); }
}
.route-step {
  position: relative;
  padding-top: var(--sp-6);
  border-top: 1px solid var(--hairline-color);
}
.route-step::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: currentColor;
  transform: translateY(-50%);
}
.route-num { color: var(--text-secondary); }
.route-title {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.05;
  margin-top: var(--sp-4);
}
/* Two of the three titles wrap to two lines and one doesn't, which left
   the school labels and body copy sitting at three different heights
   across the row. Reserving two lines on the title squares them up.
   Only once the steps are actually side by side — stacked, there is
   nothing to align to and the reserved line is just a gap. */
/* Two lines of 1.05 leading, plus the 0.2em the reveal mask pads on:
   box-sizing is border-box here, so min-height has to cover that
   padding too or the one-line title lands 6px short of the others. */
@media (min-width: 860px) {
  .route-title { min-height: calc(2 * 1.05em + 0.2em); }
}
.route-school { margin-top: var(--sp-3); color: var(--text-secondary); }
.route-step p { margin-top: var(--sp-4); color: var(--text-secondary); }
.route-note {
  margin-top: var(--sp-8);
  font-size: var(--step-1);
  color: var(--text-secondary);
  max-width: 54ch;
}
.route-note + .link-arrow { margin-top: var(--sp-6); }

/* Care band on a coloured band. The default card is --paper-2, a light
   surface, and .bg-alt re-scopes --text to --on-signal — so dropping the
   default card onto blue would put near-white text on near-white paper.
   This variant loses the fill and takes a hairline instead, so it reads
   as a panel and inherits the band's own text colours. */
.care-band.on-band { background: transparent; border: 1px solid var(--hairline-color); }

/* ── Pricing: at-a-glance summary ─────────────────────────────────── */
/* Fills the intro grid's second column with the three figures, so the
   page answers "how much" above the fold instead of making someone
   scroll to the table for it. Mono figures to match the table below. */
.price-summary { border-top: var(--hairline); padding-top: var(--sp-4); }
.price-summary dl { margin: var(--sp-3) 0 0; }
.price-summary dl > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-3) 0;
  border-bottom: var(--hairline);
}
.price-summary dt { color: var(--text-secondary); }
.price-summary dd {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
}
.price-summary .unit { color: var(--text-secondary); font-size: var(--step--1); }

/* ── Case study: onward navigation ────────────────────────────────── */
/* Tighter than a full .section — this is punctuation between the last
   chapter and the CTA, not a section in its own right. */
.cs-more { padding-block: var(--sp-7); }
.cs-more-nav {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-6);
  align-items: center;
}
@media (min-width: 800px) {
  .cs-more-nav { grid-template-columns: 1fr auto 1fr; gap: var(--sp-7); }
  /* The next link right-aligns so the pair reads as a spread, with
     "All work" holding the centre. */
  .cs-more-link.is-next { text-align: right; }
}
.cs-more-link { display: block; text-decoration: none; }
.cs-more-title {
  display: block;
  margin-top: var(--sp-2);
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
/* Arrows point the way each link goes, and lean further on hover. */
.cs-more-link.is-prev .cs-more-title::before { content: "\2190\00a0"; }
.cs-more-link.is-next .cs-more-title::after { content: "\00a0\2192"; }
.cs-more-link .cs-more-title::before,
.cs-more-link .cs-more-title::after {
  display: inline-block;
  transition: transform var(--dur-base) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .cs-more-link.is-prev:hover .cs-more-title::before { transform: translateX(-5px); }
  .cs-more-link.is-next:hover .cs-more-title::after { transform: translateX(5px); }
}
.cs-more-all {
  justify-self: start;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* The line box alone is 21px, three short of the 24px minimum target
     size. Padding rather than a height so it stays a text link. */
  padding-block: var(--sp-2);
}
@media (min-width: 800px) { .cs-more-all { justify-self: center; } }

/* ── Press feedback ───────────────────────────────────────────────── */
/* Before this, .btn was the only element on the site that reacted to
   being pressed. Everything else — the work cards, the service rows, the
   FAQ headers, the hero chips — navigated without ever acknowledging the
   press, which on a touch device is the entire interface feeling inert.

   Scale is deliberately smaller here than on a button. 0.97 on a 44px
   control reads as a press; the same value on a full-width row reads as
   the layout collapsing. Large surfaces need less.

   These are ancestors of the thing actually pressed — :active matches up
   the tree, so pressing a card's stretched link presses the card. On a
   fine pointer the JS tilt writes an inline transform that outranks
   this, but that pointer already has hover feedback; touch, which has
   none, is exactly where this lands. */
.work-card,
.cs-more-link,
.service-row,
.faq-item summary,
.hero-chip {
  transition: transform var(--dur-fast) var(--ease-out);
}
.work-card:active,
.service-row:active,
.faq-item summary:active { transform: scale(0.995); }
.cs-more-link:active,
.hero-chip:active { transform: scale(0.97); }

/* ── Per-project colour ───────────────────────────────────────────── */
/* Five different businesses — a tour operator, a pilates studio, a law
   firm, a brokerage, a clinic — were rendering in one identical palette,
   so the work index read as a list of files rather than a portfolio.
   Each project now carries its own client's colour in --project, sampled
   from that client's live site (Avoca's comes from the palette its own
   case study already documents).

   Used as fill, never as ink. Several of these cannot pass contrast as
   text: Platinum's #D7ADA0 on a white card is about 1.9:1. As a purely
   decorative spine there is no contrast requirement at all, because the
   colour carries no information the project name isn't already giving.
   Falls back to --signal wherever --project isn't set. */
.work-card,
.work-card-lg { position: relative; }

/* The face. Five borrowed client palettes were the wrong instinct — the
   bronze most obviously, but all of them diluted a brand whose own blue
   is stronger than any colour I could import. This is the site's own ink
   instead, at the scale the statement band works at, because that band
   is the best thing on the site and nothing else was pitched anywhere
   near it.

   Sitting on the blue work band, an ink face is the hardest contrast the
   palette can make: 18:1, and the numeral reads from across the room.
   It is still the screenshot slot — same 16/7 frame, ink underneath
   instead of colour. */
.project-face {
  display: flex;
  align-items: flex-end;
  aspect-ratio: 16 / 7;
  padding: var(--sp-5) var(--sp-6);
  background: #0B0B0C;
  color: #F7F5F1;
}
.project-face-num {
  font-family: var(--font-display);
  font-weight: 600;
  /* Up from a 3.6rem mono numeral. Display face, not mono: the mono set
     is for labels and data, and this is meant to be the loudest thing on
     the card. */
  font-size: clamp(3.5rem, 8vw, 6rem);
  line-height: 0.85;
  letter-spacing: -0.04em;
}
.work-card-body { padding-top: var(--sp-6); }

/* The featured band and the case study hero take a rule along the top
   edge instead — a spine on a full-bleed band would read as a stray
   vertical line rather than as that project's colour. */
.work-featured,
.cs-hero { position: relative; }
.work-featured::before,
.cs-hero::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--project, var(--signal));
}

/* The oversized numeral picks it up too, but only on the dark bands,
   where every sampled colour clears 3:1 against #0B0B0C. */
.work-featured .work-featured-num { color: var(--project, var(--signal)); opacity: 0.9; }

/* ── Final CTA ────────────────────────────────────────────────────── */
/* It said "Get a quote" and pointed at a bare contact form, discarding
   whatever had just been configured at the top of the page. It now
   carries the same brief and names the tier, so the last thing anyone
   reads continues the conversation instead of restarting it.

   The second link matters as much as the button. A form is a commitment,
   and some people who will happily send an email will not fill one in —
   there was no email path anywhere in this page's body at all. */
.cta-sub {
  margin-top: var(--sp-4);
  max-width: 44ch;
  color: var(--text-secondary);
  font-size: var(--step-1);
}
.cta-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-5) var(--sp-6);
  margin-top: var(--sp-6);
}
/* .cta-band .email-link is --signal, which is 2.6:1 on this band. The
   lifted blue the ink bands already use clears it at 5:1. */
.cta-band.bg-ink .email-link { color: #5C72FF; font-size: var(--step-0); }
.cta-band.bg-ink .link-underline { background-image: linear-gradient(#5C72FF, #5C72FF); }

.tl-cta { margin-top: var(--sp-6); }

/* ── Pricing hero ─────────────────────────────────────────────────── */
/* The page used to open on a paragraph and bury the quote builder at
   3,266px of a 5,957px page — 15 of the page's interactive controls,
   more than half way down, on a page asking for €20,000. Nobody reading
   a paragraph, a timeline, a table and a care plan first ever reached
   the one thing that would have engaged them.

   The builder is the opening now: answer first, evidence after. It also
   fixes three separate things quoting a price at you, which is most of
   why the page did not make sense — there is one number on the page now
   and you control it. */
/* Scoped to match .pricing-page .section, which would otherwise
   outrank it and put the generic band padding back on the hero. */
.pricing-page .section.config-hero { padding-block: var(--sp-9) var(--sp-8); }
.config-hero .config-heading {
  font-size: var(--step-4);
  line-height: 0.98;
  max-width: 16ch;
  margin-top: var(--sp-5);
}
.config-lead {
  margin-top: var(--sp-5);
  max-width: 46ch;
  color: var(--text-secondary);
  font-size: var(--step-1);
}
/* The configurator sits in its own band now rather than under the
   heading, so its own section padding is the space above it. */
.config-band .config { margin-top: 0; }

/* The bands were 192px of padding around content that often ran to less
   than that, which is what made the page read as sparse rather than
   spacious. Pulled in so each band is filled by what is in it. */
.pricing-page .section { padding-block: var(--sp-10); }

/* ── Comparison table: explanations and a difference filter ───────── */
.table-tools { display: flex; justify-content: flex-end; margin-bottom: var(--sp-4); }

/* The feature name becomes the control. A separate "?" affordance would
   be a 20px target beside a full-width row that is already the obvious
   thing to press. */
.feature-why {
  display: flex;
  align-items: center;
  /* Was space-between across the full column, which pinned the icon
     361px away from a 131px label and made it read as an orphaned
     control. The button keeps its full width so the target stays large;
     only the icon moves back beside the text it belongs to. */
  justify-content: flex-start;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-2) 0;
  background: none;
  border: 0;
  font: inherit;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}
.why-icon {
  position: relative;
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  opacity: 0.55;
  transition: transform var(--dur-base) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}
.why-icon::before, .why-icon::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  background: currentColor;
  transform: translate(-50%, -50%);
}
.why-icon::before { width: 12px; height: 1.5px; }
.why-icon::after { width: 1.5px; height: 12px; transition: transform var(--dur-base) var(--ease-out); }
/* Plus becomes minus by collapsing the upright, not by rotating the
   whole glyph — rotating it lands back on a plus. */
.feature-why[aria-expanded="true"] .why-icon::after { transform: translate(-50%, -50%) scaleY(0); }
.feature-why[aria-expanded="true"] .why-icon { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
  .feature-why:hover .why-icon { opacity: 1; }
}
.feature-why:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }

.price-why {
  grid-column: 1 / -1;
  padding-top: var(--sp-3);
}
.price-why p {
  color: var(--text-secondary);
  font-size: var(--step--1);
  max-width: 68ch;
}

/* Filter. Rows that match on both tiers drop out, so what is left is
   exactly what the extra money buys. */
.price-table.is-diff-only .price-row[data-differs="false"] { display: none; }

/* ── Pricing: asking for the sale ─────────────────────────────────── */
/* Not one of the three priced things had an action anywhere near it.
   The first CTA on the page arrived 5,600 characters after the price
   table began, so someone decided at the price — the moment of highest
   intent — and then had to scroll past two more sections to find
   anything to click. Each tier and the Care plan now carry their own.

   Each one prefills the contact form's budget with the band that
   matches, using the exact option strings that form offers, so nobody
   re-types a number they have already chosen. */
.tier-cta {
  display: inline-flex;
  margin-top: var(--sp-5);
  width: 100%;
}
@media (min-width: 768px) { .tier-cta { width: auto; } }
.care-cta { margin-top: var(--sp-5); align-self: flex-start; }

/* Outline on Launch, solid on Grow. The recommended tier should be the
   easier button to press, and two identical buttons would make the
   "Most chosen" label the only thing doing that work. */
.price-row.head .is-chosen .btn-solid { background: var(--signal); color: var(--paper); }

/* Named clients under the table. The strongest honest proof available
   on this page: these are the five builds in the Work section, so it
   claims nothing that isn't already evidenced there — and deliberately
   does not tie any of them to a tier, which would be an invention. */
.price-proof {
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: var(--hairline);
  color: var(--text-secondary);
  font-size: var(--step--1);
}
.price-proof strong { color: var(--text); font-weight: 500; }

/* ── Pricing configurator ─────────────────────────────────────────── */
/* The estimator was the one thing on this page people actually touched,
   so the page is built around a bigger version of it. Every choice is a
   real input with a visible pill face — the input itself stays in the
   DOM rather than being replaced by a div, so keyboard and screen reader
   users get the actual control and the styling rides on :checked. */

.config-heading {
  font-size: var(--step-3);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
  max-width: 20ch;
  margin-top: var(--sp-4);
}

/* The configurator was a bordered box sitting in a paper band — a widget
   dropped into a layout rather than a layout in its own right. It is the
   band now: the choices scrolling in the left column and the running
   quote pinned in the right, so the number stays with you the whole way
   down instead of waiting at the bottom.

   The whole point of a configurator is watching the answer move while
   you change your mind. That only works if the answer is always on
   screen. */
.config-band { position: relative; }

.config {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-8);
  margin-top: var(--sp-8);
}
@media (min-width: 960px) {
  .config { grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); gap: var(--sp-10); align-items: start; }
}

/* --sp-9 between groups made this column 157px taller than the panel
   beside it, leaving a ragged bottom edge on the page's main section. */
.config-controls { display: flex; flex-direction: column; gap: var(--sp-8); }
.config-group { border: 0; padding: 0; margin: 0; }
.config-group legend, .config-group > .mono-label { padding: 0; margin-bottom: var(--sp-5); }

.pick-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); }

/* The control is real and focusable; the pill is its face. Opacity 0
   rather than display:none so it stays in the tab order. */
.pick { position: relative; display: inline-flex; cursor: pointer; }
.pick input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.pick-face {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--sp-5);
  border: 1px solid var(--hairline-color);
  border-radius: 999px;
  font-size: var(--step--1);
  color: var(--text-secondary);
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .pick:hover .pick-face { border-color: var(--text); color: var(--text); }
}
.pick input:checked ~ .pick-face {
  background: var(--text);
  border-color: var(--text);
  color: var(--canvas);
}
.pick input:active ~ .pick-face { transform: scale(0.97); }
.pick input:focus-visible ~ .pick-face { outline: 2px solid var(--signal); outline-offset: 3px; }
/* Exclusive choices are pills, multi-select ones are rounded rectangles.
   They were identical in every measurable way — same radius, border,
   height and size — so nothing on screen said which groups let you pick
   more than one. Shape is the cheapest way to carry that, and it
   survives both themes and any colour. */
.pick input[type="checkbox"] ~ .pick-face { border-radius: var(--radius-md); }

/* .config-band is the ink band again — only the hero above it is paper —
   so the resting border needs lifting against it. Scoped to the band and
   not the page, which is what keeps it off the paper hero. */
.config-band .pick-face { border-color: rgba(247, 245, 241, 0.28); }

.config-group input[type="range"] { width: 100%; margin-top: var(--sp-2); }
.config-group output {
  display: block;
  margin-top: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-secondary);
}

/* The pinned answer. top clears the sticky header. */
.config-result { display: flex; flex-direction: column; align-items: flex-start; }
@media (min-width: 960px) {
  .config-result { position: sticky; top: 108px; }
}

/* "Best fit" is the label; this is the answer, so it stops being a
   second uppercase mono line under the first one and becomes a word you
   read. */
.config-tier {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--text);
  margin-top: var(--sp-2);
}
/* Statement scale. This is the number the entire page exists to arrive
   at, and it was set smaller than the section headings above it. */
.config-price {
  font-family: var(--font-display);
  font-size: var(--step-5);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 0.88;
  margin-top: var(--sp-3);
}
.config-monthly { font-family: var(--font-mono); font-size: var(--step--1); color: var(--text-secondary); margin-top: var(--sp-3); }

.config-includes, .config-scope ul {
  list-style: none;
  margin: var(--sp-6) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  font-size: var(--step--1);
  color: var(--text-secondary);
}
.config-includes li, .config-scope li { display: flex; align-items: center; gap: var(--sp-2); }

.config-scope { margin-top: var(--sp-6); padding-top: var(--sp-5); border-top: var(--hairline); width: 100%; }
/* Dashes, not ticks: these are the things the tier does not cover, and a
   tick beside them would read as included. */
.config-scope .tick { background: var(--text-secondary); height: 1px; }

.config-brief {
  width: 100%;
  margin-top: var(--sp-6);
  padding: var(--sp-5);
  border: 1px solid var(--hairline-color);
  border-radius: var(--radius-md);
}
.config-brief p {
  margin-top: var(--sp-3);
  font-size: var(--step--1);
  color: var(--text);
  max-width: none;
}
.config-result .btn { margin-top: var(--sp-6); }
.config-result .est-note { margin-top: var(--sp-4); }

/* ── Running total, small screens ─────────────────────────────────── */
/* The sticky rail needs a second column, so below 960px the answer would
   scroll away entirely. A pinned bar keeps it on screen instead — the
   same job the rail does, in the shape a phone can hold. It appears only
   while the configurator is in view, so it never covers the rest of the
   page. */
.quote-bar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 300;
  background: var(--canvas);
  border-top: var(--hairline);
  transform: translateY(110%);
  transition: transform var(--dur-base) var(--ease-out);
  pointer-events: none;
}
.quote-bar.is-open { transform: translateY(0); pointer-events: auto; }
@media (min-width: 960px) { .quote-bar { display: none; } }

.quote-bar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  max-width: var(--content-max);
  margin: 0 auto;
  padding: var(--sp-4) var(--page-pad);
}
.quote-bar-tier {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.quote-bar-price {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1;
  margin-top: 2px;
}

/* Same blur-bridged swap the old estimator used, on the values that
   actually change when the tier does. */
.config-tier, .config-price, .config-monthly, .config-includes {
  transition: opacity var(--dur-fast) var(--ease-out), filter var(--dur-fast) var(--ease-out);
}
.config-result.is-swapping .config-tier,
.config-result.is-swapping .config-monthly,
.config-result.is-swapping .config-includes { opacity: 0.4; filter: blur(3px); }
/* The price gets more than a fade. It is the thing people are moving the
   controls to see change, so it lifts and blurs on the way out and
   arrives from below — the figure reads as being replaced rather than
   redrawn. */
.config-result.is-swapping .config-price {
  opacity: 0;
  filter: blur(6px);
  transform: translateY(-14px);
}
.config-price { transition: opacity var(--dur-fast) var(--ease-out), filter var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out); }


/* ── Pricing: project timeline ────────────────────────────────────── */
/* The closest honest answer to "a UI that shows you building a website".
   Five stages of a real project, every one of them a fact already
   published on this site. It replaces three static rows that said the
   same things without ever being touched. */
.timeline-heading {
  font-size: var(--step-3);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
  max-width: 20ch;
  margin-top: var(--sp-4);
}
.timeline { margin-top: var(--sp-8); }

.timeline-track {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--sp-2);
  position: relative;
}
/* The rail the nodes sit on, behind them. */
.timeline-track::before {
  content: "";
  position: absolute;
  top: 10px;
  left: 10%;
  right: 10%;
  height: 1px;
  background: var(--hairline-color);
}

.tl-step {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  padding-bottom: var(--sp-2);
  cursor: pointer;
  text-align: center;
}
.tl-step input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.tl-node {
  width: 20px;
  height: 20px;
  border-radius: 999px;
  border: 1px solid var(--text-secondary);
  background: var(--canvas);
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.tl-label {
  font-family: var(--font-mono);
  font-size: var(--step-0);
  color: var(--text-secondary);
  transition: color var(--dur-fast) var(--ease-out);
}
.tl-step.is-passed .tl-node { background: var(--text-secondary); border-color: var(--text-secondary); }
.tl-step input:checked ~ .tl-node { background: var(--text); border-color: var(--text); transform: scale(1.35); }
.tl-step input:checked ~ .tl-label { color: var(--text); }
@media (hover: hover) and (pointer: fine) {
  .tl-step:hover .tl-node { border-color: var(--text); }
  .tl-step:hover .tl-label { color: var(--text); }
}
.tl-step input:focus-visible ~ .tl-node { outline: 2px solid var(--signal); outline-offset: 3px; }
/* Five columns across a phone is 59px each, and "Year one" wraps to two
   lines at the larger label size. The bigger type is there to stop the
   track reading as tiny on a wide screen; on a narrow one the track is
   not the problem. */
@media (max-width: 699px) {
  .tl-label { font-size: var(--step--1); }
  .tl-node { width: 16px; height: 16px; }
  .timeline-track::before { top: 8px; }
}

.timeline-panel {
  margin-top: var(--sp-7);
  padding-top: var(--sp-6);
  border-top: var(--hairline);
  /* Reserved so stepping through stages of different lengths does not
     shunt the section below up and down. */
  min-height: 11rem;
}
.timeline-panel h3 {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 600;
  letter-spacing: -0.02em;
  margin-top: var(--sp-3);
}
.timeline-panel p { margin-top: var(--sp-4); color: var(--text-secondary); max-width: 56ch; font-size: var(--step-1); }

/* ── Pricing: the year-one choice ─────────────────────────────────── */
/* Two options the site already commits to, so this compares rather than
   frightens: stay on a Care plan, or take a clean handover. */
.care-switch { display: flex; flex-direction: column; gap: var(--sp-4); }
.care-switch .care-blurb { margin: 0; }
.care-switch ul { min-height: 5.5rem; }

/* ── Services: running tally ──────────────────────────────────────── */
/* Slides in once something is picked and carries the count down the
   page with you. Hidden and inert until then, so it never covers the
   page for someone who has not started. */
.brief-bar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 300;
  background: var(--canvas);
  border-top: var(--hairline);
  transform: translateY(110%);
  transition: transform var(--dur-base) var(--ease-out);
  pointer-events: none;
}
.brief-bar.is-open { transform: translateY(0); pointer-events: auto; }
.brief-bar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  max-width: var(--content-max);
  margin: 0 auto;
  padding: var(--sp-4) var(--page-pad);
}
.brief-bar-count {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.02em;
}
@media (prefers-reduced-motion: reduce) {
  .brief-bar { transition: none; }
}

/* ── Contact form ─────────────────────────────────────────────────── */

/* No padding of its own: the .section it sits in already pads, and the
   two stacked to 192px of empty blue above the first field on a phone. */
.contact-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-9); align-items: start; }
@media (min-width: 1024px) { .contact-grid { grid-template-columns: minmax(0,7fr) minmax(0,5fr); gap: var(--sp-10); } }

.contact-form { display: flex; flex-direction: column; gap: var(--sp-6); }

.field-row { display: grid; grid-template-columns: 1fr; gap: var(--sp-5); }
@media (min-width: 480px) { .field-row { grid-template-columns: minmax(0,1fr) minmax(0,1fr); } }

.field { display: flex; flex-direction: column; gap: var(--sp-2); }
.field label { font-size: var(--step--1); font-weight: 500; color: var(--text); }
.field .hint { font-size: var(--step--1); color: var(--text-secondary); font-weight: 400; }
.field input, .field select, .field textarea {
  height: 48px;
  padding: 0 var(--sp-4);
  border: var(--hairline);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: var(--step-0);
  color: var(--text);
  background: var(--surface);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible { border-color: var(--signal); }
.field textarea { height: auto; min-height: 140px; padding: var(--sp-3) var(--sp-4); line-height: 1.6; resize: vertical; }

/* Error state — border colour AND explicit prose, never colour alone. */
.field.has-error input, .field.has-error select, .field.has-error textarea { border-color: #C0392B; }
/* display cannot transition, so this used to pop in — an element
   appearing from nothing, at the one moment the person is already
   annoyed. allow-discrete plus @starting-style is what makes a
   display-toggled element animate: the browser flips display at the
   start of the transition rather than the end, and @starting-style gives
   it a from-state to move out of. Unsupported browsers ignore both and
   get today's instant behaviour, which is the correct fallback. */
.field-error {
  display: none;
  font-size: var(--step--1);
  color: #C0392B;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              display var(--dur-fast) allow-discrete;
}
.field.has-error .field-error {
  display: block;
  opacity: 1;
  transform: translateY(0);
}
@starting-style {
  .field.has-error .field-error { opacity: 0; transform: translateY(-4px); }
}

.contact-aside { display: flex; flex-direction: column; gap: var(--sp-8); }
/* No colour here on purpose. This rule sits after .bg-alt .email-link
   at equal specificity, so declaring --signal made the address the exact
   colour of the band behind it — 1.0:1, invisible. The band rule now
   supplies the colour; this one only sets the type. */
.contact-aside .email-link { font-family: var(--font-display); font-weight: 500; font-size: var(--step-1); }
.contact-aside p { color: var(--text-secondary); max-width: 40ch; }

.next-steps { display: flex; flex-direction: column; gap: var(--sp-5); border-top: var(--hairline); padding-top: var(--sp-5); }
.next-steps .mono-label { margin-bottom: var(--sp-2); }
.next-steps p { color: var(--text-secondary); }

/* Success state replaces the form in place — no navigation away, so a
   submitter never loses context or has to use the back button. */
.form-success { display: none; padding: var(--sp-7); border: var(--hairline); border-radius: var(--radius-md); background: var(--surface); }
.form-success.is-visible { display: block; }
.form-success h3 { font-size: var(--step-1); font-weight: 500; margin-bottom: var(--sp-3); }
.form-success p { color: var(--text-secondary); }
.contact-form.is-submitted { display: none; }

.form-status { font-size: var(--step--1); color: #C0392B; }
.form-status:empty { display: none; }

/* ── Version switch (Classic / New) ─────────────────────────────────
   Both designs are live side by side: the original at the site root,
   the poster redesign under v2/. Every page links to its twin. Plain
   links, so it needs no JS, and the theme choice (sessionStorage, same
   origin) carries across. In the header from 600px; below that the
   header has no room, so it sits in the menu panel instead. The border
   reads --hairline-color directly: --hairline resolves on :root. */
.version-switch {
  display: none;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--hairline-color);
  border-radius: 999px;
  flex-shrink: 0;
}
@media (min-width: 600px) { .nav-actions > .version-switch { display: inline-flex; } }
.version-switch a {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 14px;
  border-radius: 999px;
  font-size: var(--step--1);
  font-weight: 500;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  color: var(--text-secondary);
  transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.version-switch a[aria-current="true"] { background: var(--text); color: var(--canvas); }
@media (hover: hover) and (pointer: fine) { .version-switch a:not([aria-current]):hover { color: var(--text); } }
@media (pointer: coarse) { .version-switch a { min-height: 44px; } }
.mobile-menu-panel .version-switch { display: inline-flex; align-self: flex-start; margin-top: var(--sp-5); }
.mobile-menu-panel .version-switch a { font-family: var(--font-body); font-size: var(--step-0); font-weight: 500; letter-spacing: 0; padding-block: 0; }
@media (min-width: 600px) { .mobile-menu-panel .version-switch { display: none; } }
