/* =============================================================================
   Austra Digital v12 — layout/type system measured from the licensed Framer
   reference, recoloured to Austra cobalt, plus the v12 motion/interaction layer
   specified in docs/ANIMATION-BRIEF-v12.md.

   Rules held throughout:
     · every pre-animation hidden state is gated behind .js-enabled, so with JS
       disabled the page renders fully visible
     · .js-fallback (set by the <head> net) force-unhides everything
     · prefers-reduced-motion collapses every sequence to its final visible state
     · no animation of width/height/top/left during scroll — transform+opacity only
   ========================================================================== */

/* Self-hosted Archivo (variable, latin subset). Single file covers 300–800;
   no Google Fonts dependency — fully offline. */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/archivo.woff2') format('woff2');
}

:root {
  /* surfaces */
  --bg:        #fbfbfc;
  --card:      #ffffff;
  --band:      #f1f2f4;
  --ink:       #131519;   /* deep charcoal — CTA band */
  /* text */
  --text:      #24272d;
  --muted:     #4e535b;
  --tertiary:  #666c76;
  /* lines + accent
     v15 keeps v14's monochrome discipline — labels stay silver-grey — and gives
     ONE colour to the things that mean "act" or "active": CTAs, prices, ticks,
     the rail, active states.

     Emerald is applied as a GRADIENT on surfaces (buttons, badges, dots, rails)
     and as a SOLID on text, because a gradient on text makes contrast
     unverifiable. --accent is the solid; --grad-* are the surface stops. */
  --hairline:  #d6d9de;
  --accent:    #3d444e;   /* brushed steel — 9.5:1 on the page, 8.8:1 on band sections */
  --accent-dk: #2f353d;
  --accent-mid:#5a616c;   /* silver-grey, for labels — unchanged from v14 */

  /* SILVER FOLLOWS THE GOLD INVERSION, NOT THE COBALT RULE.
     Polished metal is a LIGHT surface, so it cannot carry white text — it
     carries a near-black label. And silver as TEXT is impossible: a darkened
     silver is just grey, so the solid becomes the brushed steel above.

     v17b lifts the whole ramp. The material still comes from --metal-inset,
     not from colour, but the face now sits close to the page at the lit corner
     and only separates at the shaded one. The SILHOUETTE is carried by the
     bottom stop plus the cast shadow — not by darkening the face.

     Stops vs --on-metal: #f8fafc = 17.3 · #edf1f6 = 16.0 · #cfd6e0 = 12.4.
     Composited under the label, worst case in any state: 13.0:1.
     Darkest composited face pixel vs the page: 1.63:1, down from 2.11:1 in
     v17a — lighter, but still a defined object. An earlier attempt that ENDED
     light (~1.06:1 everywhere) dissolved into the page; this one only STARTS
     light. That is the whole difference. */
  --on-metal:  #14161a;   /* the label colour on any metal surface */
  --gem-lit:   #ffffff;   /* brightest polish — decoration only (hero rail) */
  --grad-a:    #f8fafc;   /* lit corner    — 17.3:1 vs --on-metal */
  --grad-m:    #edf1f6;   /* body          — 16.0:1 */
  --grad-b:    #cfd6e0;   /* shaded corner — 12.4:1, and 1.42:1 vs the page */
  --grad:      linear-gradient(135deg, var(--grad-a) 0%, var(--grad-m) 48%, var(--grad-b) 100%);

  /* Hover brightening, painted on ::after and cross-faded with OPACITY —
     background-image does not interpolate, and opacity is the one property
     this build is happy to animate. Directional, so the shaded corner (and
     therefore the silhouette) survives the hover. */
  --metal-sheen: linear-gradient(135deg,
                   rgba(255, 255, 255, .62) 0%,
                   rgba(255, 255, 255, .22) 46%,
                   rgba(255, 255, 255, 0)  100%);

  --metal-edge:rgba(20, 22, 26, .16);   /* the cut edge — used by .pc-badge */
  --metal-back:#aab2be;   /* fallback body only; 8.5:1 vs --on-metal if ::before
                             ever fails to paint. No longer shows as the rim. */
  --metal-rim: rgba(72, 80, 92, .55);   /* the pressed rim. A rim reads as
                             pressed because it is darker THAN THE FACE, not
                             because it is near-black. Steel, not #3a414b. */

  /* THE MATERIAL. All in em so .btn-sm and .drawer-cta get a proportionally
     smaller bevel. Two dark layers pulled back (.22->.13, .20->.12) and two
     white layers pushed up (.30->.42, .28->.34) — the dome survives, the
     weight does not. */
  --metal-inset:
    -0.05em -0.05em 0.05em 0    inset rgba(20, 22, 26, .13),
     0      0      0.05em 0.2em inset rgba(255, 255, 255, .42),
     0.025em 0.05em 0.1em  0    inset rgba(255, 255, 255, 1),
     0.12em  0.12em 0.12em      inset rgba(255, 255, 255, .34),
    -0.075em -0.22em 0.22em 0.08em inset rgba(20, 22, 26, .12);

  /* PRESSED. Layer 1 is a spread-only inset ring — it draws the rim by
     REPAINTING, not by moving geometry. So `inset` never animates (the file's
     own header rule forbids animating top/left), border-radius draws and
     antialiases the curve itself, and the em unit still scales it. This
     replaces BOTH the reference's clip-path and v17a's inset:-1px->1px. */
  --metal-inset-in:
     0       0      0      0.075em inset var(--metal-rim),
     0.09em  0.13em 0.10em 0       inset rgba(20, 22, 26, .28),
    -0.03em -0.04em 0.07em 0.02em  inset rgba(20, 22, 26, .22),
     0.20em  0.20em 0.20em 0       inset rgba(20, 22, 26, .16),
     0       0      0.05em 0.42em  inset rgba(255, 255, 255, .22),
    -0.075em -0.12em 0.20em 0.10em inset rgba(20, 22, 26, .12);
  --metal-lite:#d8dee6;   /* silver on charcoal — 13.5:1 on the CTA band */
  /* facet colours for the .gem field — LIGHT-SURFACE set (must be darker than
     the page or they vanish). Flipped to a light set on the charcoal band. */
  --gf-a:      #c0c7d1;
  --gf-b:      #9aa3ae;
  --gf-c:      #6f7784;
  --silver:    #c3c9d1;
  --silver-lt: #e7eaee;

  /* radii */
  --r-card:  40px;
  --r-round: 99px;

  /* rhythm */
  --container: 1440px;
  --pad:       96px;
  --sec-gap:   72px;
  --grid-gap:  20px;
  --lift:      48px;      /* section overlap */

  /* INTERACTIVE BOUNDARIES — one ramp, three steps, mirroring the primary's
     rest -> hover -> press escalation. Every value clears 3:1 (WCAG 1.4.11) on
     all three light surfaces the page uses. --hairline (1.37:1 on the page,
     1.26:1 on --band) stays DECORATIVE and is never a control boundary again.

       --edge     3.36 page · 3.47 card · 3.10 band
       --edge-on  4.56 page · 4.72 card · 4.21 band
       --accent   9.51 page · 9.83 card · 8.78 band

     #8b919b (v14's value) was retired from control borders: it measures 2.83:1
     on --band, and .btn-ghost and .faq-ico both sit on --band. */
  --edge:    #848a94;
  --edge-on: #6d747e;

  /* The focus ring colour is a VARIABLE so dark surfaces can re-point it.
     --accent is 9.51:1 on the page but 1.86:1 on the footer ink — a focus
     indicator you cannot see is not a focus indicator. */
  --focus:   var(--accent);

  /* ---- SHADOW TEST (v17e) — REVERT BY SETTING THESE FOUR BACK ---------------
     was: --sh-a .30  --sh-b .34  --sh-hover .40  --sh-lift .26
     Deeper hover shadow on light sections, and the same shape inverted to WHITE
     on dark ones — on --ink a black shadow is invisible, so a raised object on
     the CTA band and the footer had no lift at all. Every consumer reads these
     tokens, so this whole experiment is four numbers wide. */
  --sh-a:      .42;
  --sh-b:      .48;
  --sh-hover:  .58;
  --sh-lift:   .36;
  --sh:        rgba(20, 30, 70, var(--sh-b));
  --sh-tight:  rgba(20, 30, 70, var(--sh-a));
  --sh-deep:   rgba(20, 30, 70, var(--sh-hover));
  --sh-ghost:  rgba(20, 30, 70, var(--sh-lift));

  /* motion */
  --e-out:  cubic-bezier(.22, 1, .36, 1);
  --e-back: cubic-bezier(.34, 1.4, .64, 1);

  /* ---- MARKER FAMILY -------------------------------------------------------
     Every circular marker on this site is one of two things: a STATE DOT with
     nothing inside it, or a MARK DISC that carries a glyph. Before this there
     were 8 diameters, 5 ring specs, 7 transition lists and 5 "active" scales
     across 14 markers. Sizes still differ by role — a 1px-rail dot and a 64px
     numbered token are not the same object — but the GRAMMAR is now shared:
     one radius, one ring rule, three states, one active increment.

     Tier A states:  idle = hairline ring, no fill
                     done = filled silver, silver ring
                     live = metal face + accent ring + halo (an ::after, so it
                            is revealed by OPACITY and can therefore animate) */
  --dot-xs:    8px;    /* inline with text: .status-pill .dot, .ts-dot        */
  --dot-sm:    9px;    /* on the hero rail: .fn-dot — 9 keeps top:3 centred
                          on the 1px rail at 7.5px. Do not change to 10.      */
  --dot-md:    14px;   /* sequence nodes: .sn-dot, .badge-core                */
  --mark-md:   22px;   /* glyph discs: .tick, .cmp-x                          */
  --dot-ring:      2px;     /* >= 12px */
  --dot-ring-thin: 1.5px;   /* <  12px */
  --dot-idle:  var(--hairline);
  --dot-done:  #aab2bd;
  --dot-mute:  #e3e6ea;     /* the recede, on decorative graphics only        */
  --dot-pop:   1.15;   /* tier A */
  --mark-pop:  1.08;   /* tier B — 15% of 22px shoves the baseline, 15% of 8px
                          does not. Equal visual increment, not equal factor. */
  --dot-t: box-shadow .3s linear, background-color .3s linear,
           transform .3s var(--e-back);

  /* The ONLY two hues on this site, and they exist for exactly one component:
     the x/tick comparison, where a red and a green are adjacent and opposed.
     Chosen for LIGHTNESS as much as hue — #a3453b sits at --tertiary's
     greyscale value and #1c5340 at --accent's, so desaturating the page
     preserves the existing "left column is lighter" hierarchy (1.48:1 grey).
     Measured: x 5.07:1 on its disc, tick 7.30:1 on its disc; both >= 3:1 for
     WCAG 1.4.11 and both >= 4.5:1 if ever used as text.
     NOTE: for a protanope these two are 1.11:1 — the SAME COLOUR. The glyph
     shape carries the meaning; colour is a bonus for those who can see it.
     Never reduce these marks to two coloured dots. */
  --mark-no:  #a3453b;
  --mark-yes: #1c5340;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'Archivo', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  font-size: 18px;
  font-weight: 300;
  line-height: 1.5;
}

img, svg { max-width: 100%; }

h1, h2, h3, h4, h5 { margin: 0; font-weight: 600; letter-spacing: normal; }
/* h1-h5 set margin, weight and tracking but never line-height, so 33 heading
   elements were inheriting body's 1.5 — a 34px heading carrying 51px of leading.
   Specificity (0,0,1), so any component that already decided still wins. */
h3, h4, h5 { line-height: 1.2; }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }

/* The skip link was a metal surface with no material — background: var(--grad)
   and no shadow, so it read as a flat swatch rather than the same object as the
   button. The bevel is in em and body font-size is 18px, so it scales for free. */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background-image: var(--grad);
  background-color: var(--metal-back);
  color: var(--on-metal);
  padding: 12px 20px; border-radius: 0 0 var(--r-round) 0;
  box-shadow: var(--metal-inset),
              0 0.40em 0.90em -0.55em var(--sh);
}
.skip:focus { left: 0; }   /* :focus, not :focus-visible — a skip link must show
                              for every focus path, and is never mouse-focused. */

/* Focus ring. Drawn 3px OUTSIDE the control, so on .btn-primary it lands on the
   page background, never on the metal face. outline follows border-radius, so
   pills get a pill ring with no clip-path. */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

/* Arriving at /#how from another page, the browser's native hash jump lands the
   section at 0 — underneath the 112px fixed header. Clicking the same link
   in-page, Lenis lands it at 112. Same anchor, two different results depending
   on how you got there. */
section[id], .cta-band[id], main[id] { scroll-margin-top: 112px; }

/* =============================================================================
   MARKER FAMILY — the shared recipe. Tokens live in :root.
   ========================================================================== */
.status-pill .dot, .ts-dot, .hero-node, .sn-dot, .badge-core {
  border-radius: var(--r-round);
  background: var(--dot-fill, transparent);
  box-shadow: inset 0 0 0 var(--dot-ring, 2px) var(--dot-line, var(--dot-idle));
  transition: var(--dot-t);
}
.status-pill .dot, .ts-dot { position: relative; display: inline-block; flex: none; }

/* THE LIVE OVERLAY. One rule, one property: "lit" is opacity 1, everywhere.
   A background-image cannot be interpolated, so the metal face has to live on
   a pseudo if any state is ever going to animate — and on the hero rail it is. */
.status-pill .dot::after, .ts-dot::after, .hero-node::after,
.sn-dot::after, .badge-core::after {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit; pointer-events: none;
  background-image: var(--grad);
  box-shadow: inset 0 0 0 var(--dot-ring, 2px) var(--accent),
              0 0 0 3px rgba(61, 68, 78, .13);
  opacity: 0;
  transition: opacity .3s linear;
}
.status-pill .dot, .ts-dot { width: var(--dot-xs); height: var(--dot-xs); --dot-ring: var(--dot-ring-thin); }
.sn-dot, .badge-core       { width: var(--dot-md); height: var(--dot-md); }

/* Dark surfaces re-point the token. --metal-lite is 13.50:1 on the footer ink.
   .qform is a WHITE panel nested inside .cta-band, so it must reset to --accent
   (--metal-lite would be 1.35:1 on white). Inheritance handles the nesting. */
.footer, .cta-band, #system { --focus: var(--metal-lite); }
.qform, .form-done { --focus: var(--accent); }

/* ---------- layout primitives ---------- */
.container {
  max-width: var(--container);
  margin: 0 auto;
  padding-left: var(--pad);
  padding-right: var(--pad);
}

.section { padding: var(--sec-gap) 0; position: relative; }

/* Layered section transition: each section rises over the previous one with
   rounded top corners. Backgrounds alternate so the seam is visible. */
.sec-lift {
  border-radius: var(--r-card) var(--r-card) 0 0;
  margin-top: calc(var(--lift) * -1);
  padding-top: calc(var(--sec-gap) + var(--lift));
  background: var(--bg);
  z-index: 1;
}
#system, #process, #team, #faq { background: var(--band); }

/* ---------- type scale ---------- */
/* Was a flat 42px in an otherwise fluid file, which made the page's h1 SMALLER
   than its h2 from 621px to 1167px (ratio 0.81 at 621-900), with an 89% jump
   across the single pixel between 620 and 621. Fluid, so the ladder holds at
   every width. -0.015em tracking re-wraps nothing (verified at 10 viewports —
   negative tracking can only remove wraps, never add them). */
h2 { font-size: clamp(30px, 2.9vw, 42px); font-weight: 600; line-height: 1.05; letter-spacing: -.015em; text-transform: uppercase; }

.eyebrow {
  font-size: 18px; font-weight: 300; line-height: 1.5;
  color: var(--accent-mid); margin-bottom: 14px;
}
.lead { font-size: 18px; font-weight: 300; line-height: 1.5; color: var(--muted); }

.sec-head { max-width: 940px; margin-bottom: 44px; }
.sec-lead { margin-top: 18px; max-width: 68ch; }

/* ---------- buttons ---------- */
.btn {
  position: relative; z-index: 0;
  display: inline-flex; align-items: center; gap: 9px;
  font-size: 18px; font-weight: 400; line-height: 1;
  padding: 18px 30px;
  border-radius: var(--r-round);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background-color .3s var(--e-out), color .3s var(--e-out),
              border-color .3s var(--e-out), transform .35s var(--e-out),
              box-shadow .35s var(--e-out);
}
.btn-primary {
  isolation: isolate;
  background: var(--metal-back);       /* fallback body only — never seen */
  color: var(--on-metal);
  border-color: transparent;
  font-weight: 500;
  /* the site's shadow family: blue-tinted, wide blur, heavy negative spread.
     In em so .btn-sm scales with it. This is .review's shadow, sized down. */
  box-shadow: 0 0.06em 0.10em -0.045em var(--sh-tight),
              0 0.40em 0.90em -0.55em  var(--sh);
  /* --e-back for the lift (matches .sn-dot / .review), --e-out for the shadow,
     linear for colour. No `ease` — it appears nowhere else in this file. */
  transition: transform  .35s var(--e-back),
              box-shadow .35s var(--e-out),
              color      .3s  linear;
}
/* THE FACE. -1px, not 0: an absolutely positioned pseudo resolves inset
   against the PADDING box, so at inset:0 the .btn 1px border leaks the body.
   inset is now CONSTANT in every state — the press is box-shadow only. */
.btn-primary::before {
  content: ""; position: absolute; inset: -1px; z-index: -1;
  border-radius: inherit;
  background-image: var(--grad);
  box-shadow: var(--metal-inset);
  transition: box-shadow .35s var(--e-out);
}
/* THE SHEEN. The third layer, free on .btn. Opacity only. */
.btn-primary::after {
  content: ""; position: absolute; inset: -1px; z-index: -1;
  border-radius: inherit; pointer-events: none;
  background-image: var(--metal-sheen);
  opacity: 0;
  transition: opacity .35s linear;
}
/* HOVER = lift and brighten, like every other hover on this site. The cast
   shadow GROWS (cards gain shadow on hover here; nothing loses it). Geometry
   does not change, so there is no jitter and no disagreement with .arw, which
   is also moving outward at .38s var(--e-out). */
.btn-primary:hover, .btn-primary:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 0.06em 0.10em -0.045em var(--sh-ghost),
              0 0.72em 1.45em -0.60em  var(--sh-deep);
}
.btn-primary:hover::after, .btn-primary:focus-visible::after { opacity: 1; }

/* .btn:active MUST stay ABOVE .btn-primary:active — identical specificity, so
   the later rule wins. Having it below silently killed the primary's press. */
.btn:active { transform: scale(.985); box-shadow: none; }

/* ACTIVE = the press, which is where the skeuomorphism belongs. The face takes
   the pressed stack including its rim ring, the lift is given back, and the
   button settles onto the page. .985 matches .btn — one press speed for both
   buttons sitting together in .hero-cta. .12s because a press under the finger
   is a state change, not a transition. */
.btn-primary:active {
  transform: translateY(0) scale(.985);
  box-shadow: 0 0.045em 0.09em -0.03em var(--sh);
  transition-duration: .12s;
}
.btn-primary:active::before { box-shadow: var(--metal-inset-in); transition-duration: .12s; }
.btn-primary:active::after  { opacity: .3;                       transition-duration: .12s; }
.btn-sm { font-size: 16px; padding: 12px 22px; }
.btn-block { width: 100%; justify-content: center; }

/* SECONDARY — the primary's gesture, one volume down.
   The old hover filled solid var(--text): a 14.5:1 slab standing next to a metal
   face whose darkest pixel is 1.63:1 against the page. The secondary action became
   the highest-contrast object on the screen, so the eye went to "See client work"
   instead of the primary CTA. And the old REST border was --hairline at 1.37:1 —
   below the 3:1 that identifies a control at all, which is WHY hover had to shout.
   Separation is now by MATERIAL, not gesture: the primary is the only filled
   object with a cast shadow, in every state. */
.btn-ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--edge);      /* 3.36 page · 3.47 card · 3.10 band */
  overflow: hidden;
  transition: color .3s linear, border-color .3s linear,
              transform .35s var(--e-back), box-shadow .35s var(--e-out);
}
/* border-radius on the pseudo, not just overflow:hidden — the curve is then drawn
   and antialiased by border-radius (no clip-path, no rectangular overflow clip). */
.btn-ghost::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit;
  background: rgba(61, 68, 78, .09);
  transform: scaleX(0); transform-origin: left center;
  transition: transform .4s var(--e-out), box-shadow .35s var(--e-out);
}
/* label 12.54:1 on the composited fill · border 8.24:1 · fill vs page 1.15:1 */
.btn-ghost:hover, .btn-ghost:focus-visible {
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: 0 0.30em 0.60em -0.55em var(--sh-ghost);
}
.btn-ghost:hover::before, .btn-ghost:focus-visible::before { transform: scaleX(1); }

/* MUST declare transform explicitly and MUST stay below :hover. .btn:active is
   further up the file at identical specificity, so a :hover transform with no
   matching :active transform silently kills the press — the same trap that killed
   .btn-primary:active in v17a. */
.btn-ghost:active {
  transform: translateY(0) scale(.985);
  border-color: var(--accent-dk);
  box-shadow: none;
  transition-duration: .12s;
}
.btn-ghost:active::before {
  transform: scaleX(1);
  box-shadow: inset 0 .09em .16em -.02em rgba(20, 22, 26, .18);
  transition-duration: .12s;
}

/* DISABLED — main.js sets #formSubmit.disabled during submit, and there was NO
   disabled style at all: measured live, the button kept cursor:pointer, the full
   metal face, the full cast shadow and a 12.38:1 label — identical to armed.
   opacity is deliberately NOT used: this build's verification counts text below
   opacity 1, and dimming a label is what the five safety nets exist to prevent.
   The light goes out of the MATERIAL instead. Label 12.38 -> 6.54:1, still well
   over AA, and main.js swaps it to "Sending…" so it says so in words too. */
.btn:disabled, .btn[aria-disabled="true"] { cursor: default; }
.btn:disabled:hover, .btn:disabled:active,
.btn[aria-disabled="true"]:hover { transform: none; }

.btn-primary:disabled,
.btn-primary:disabled:hover,
.btn-primary:disabled:focus-visible,
.btn-primary:disabled:active {
  color: var(--muted);
  background: #e9ecf1;
  box-shadow: none;
  transform: none;
}
.btn-primary:disabled::before {
  background-image: none;
  background-color: #e9ecf1;
  box-shadow: 0 0 0 1px inset var(--silver);
}
.btn-primary:disabled::after,
.btn-primary:disabled:hover::after { opacity: 0; }
.btn-primary:disabled .arw::before { transform: none; opacity: 1; }
.btn-primary:disabled .arw::after  { opacity: 0; }

/* arrow swap: existing arrow exits upper-right, replacement enters lower-left */
.arw {
  position: relative; display: inline-block; flex: none;
  width: 1.05em; height: 1.25em; overflow: hidden; line-height: 1.25em;
  font-size: 1em;
}
.arw::before, .arw::after {
  content: '\2192';
  position: absolute; inset: 0;
  text-align: center; line-height: 1.25em;
  transition: transform .38s var(--e-out), opacity .3s linear;
}
.arw::after { transform: translate3d(-110%, 110%, 0); opacity: 0; }
.btn:hover .arw::before, .btn:focus-visible .arw::before { transform: translate3d(110%, -110%, 0); opacity: 0; }
.btn:hover .arw::after,  .btn:focus-visible .arw::after  { transform: none; opacity: 1; }
.arw-sm { font-size: .95em; }

/* =============================================================================
   NAV — transparent rounded shell → translucent blur, hides on scroll-down
   ========================================================================== */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  padding-top: 20px; padding-bottom: 4px;
  transition: transform .35s cubic-bezier(.4, 0, .2, 1), padding-top .3s var(--e-out);
}
.nav-shell {
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  height: 66px; padding: 0 10px 0 22px;
  border: 1px solid transparent; border-radius: var(--r-round);
  transition: background-color .35s var(--e-out), border-color .35s var(--e-out),
              box-shadow .35s var(--e-out), backdrop-filter .35s var(--e-out);
}
.nav.is-stuck { padding-top: 12px; }
.nav.is-stuck .nav-shell {
  background: rgba(252, 252, 252, .84);
  -webkit-backdrop-filter: blur(16px);
          backdrop-filter: blur(16px);
  border-color: var(--hairline);
  box-shadow: 0 12px 34px -22px rgba(20, 30, 70, .38);
}
.nav.is-hidden { transform: translateY(-130%); }

.brand { display: inline-flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 600; }
.brand-mark { width: 18px; height: 18px; border-radius: var(--r-round); background: var(--grad); display: inline-block; }

.nav-links { display: flex; gap: 32px; }
.nav-links a { font-size: 17px; font-weight: 300; color: var(--muted); transition: color .25s linear, transform .25s var(--e-out); display: inline-block; }
.nav-links a:hover, .nav-links a:focus-visible { color: var(--text); transform: translateY(-1px); }
/* a lifted thing must come back down when you press it — same .12s as the button */
.nav-links a:active { transform: translateY(0); transition-duration: .12s; }
.nav-links a.is-active { color: var(--text); }

.nav-right { display: flex; align-items: center; gap: 14px; }

/* Below 900px this is the primary navigation control and it had a 1.37:1
   boundary, no hover and no press. Press is scale(.94), not .985: the site's
   press is ~3px of travel on a 200-330px button, and .985 on a 44px square is
   0.7px — sub-pixel, i.e. no feedback. Constant displacement, not constant ratio. */
.burger {
  display: none;
  width: 44px; height: 44px; border-radius: var(--r-round);
  border: 1px solid var(--edge); background: var(--card);
  cursor: pointer; align-items: center; justify-content: center; flex-direction: column; gap: 5px;
  transition: border-color .25s linear, background-color .25s linear,
              transform .25s var(--e-back);
}
.burger span { display: block; width: 18px; height: 1px; background: var(--text); transition: transform .3s var(--e-out); }
.burger:hover { border-color: var(--edge-on); background: #f3f5f8; }
.burger:active { transform: scale(.94); transition-duration: .12s; }
.burger[aria-expanded="true"] { border-color: var(--accent); }
.burger[aria-expanded="true"] span:first-child { transform: translateY(3px) rotate(45deg); }
.burger[aria-expanded="true"] span:last-child  { transform: translateY(-3px) rotate(-45deg); }

.drawer {
  position: fixed; inset: 86px 0 0; z-index: 99;
  display: flex; flex-direction: column; align-items: stretch;
  padding: 0 var(--pad) calc(16px + env(safe-area-inset-bottom, 0px));
  /* the scrim IS the outside-click target — without a surface below the panel
     there is nothing to click and no signal that the page is suspended */
  background: rgba(19, 21, 25, .42);
}
.drawer[hidden] { display: none; }
.drawer-inner {
  background: rgba(252, 252, 252, .96);
  -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
  border: 1px solid var(--hairline); border-radius: 28px;
  padding: 22px 24px 26px;
  display: flex; flex-direction: column; gap: 16px;
  flex: 0 1 auto; max-height: 100%;
  overflow-y: auto; overscroll-behavior: contain;
}
.drawer a { font-size: 20px; color: var(--muted); transition: color .25s linear; }
/* nav-links and foot-col both darken on hover AND focus; the drawer had neither. */
.drawer a:not(.btn):hover, .drawer a:not(.btn):focus-visible { color: var(--text); }
.drawer .drawer-cta { align-self: flex-start; color: var(--on-metal); }

/* =============================================================================
   1. HERO
   ========================================================================== */
/* 132, not 156: the nav shell ends at 86px, so this still leaves 46px of clear air
   above the pill, and it is the SAME value the <=900px breakpoint already uses —
   one number for the hero's top inset instead of two. Buys 24px of fold, which is
   what puts the whole rail plus its captions above a 900px viewport. */
.hero { position: relative; overflow: hidden; padding: 132px 0 var(--sec-gap); }
.hero-bg {
  position: absolute; left: -8%; right: -8%; top: -14%; height: 128%;
  background:
    radial-gradient(46% 42% at 72% 12%, rgba(61, 68, 78, .07), transparent 70%),
    radial-gradient(52% 46% at 12% 44%, rgba(120, 130, 155, .12), transparent 72%);
  z-index: 0; pointer-events: none;
}

/* --- faceted gem field ---------------------------------------------------
   HARD-stop conic gradients radiating from a point, like the cut of a stone —
   here reading as polished metal catching light rather than a gem. The drama in a gem comes from sharp luminance jumps
   between adjacent facets, so this is deliberately NOT heavily blurred — a
   soft wash would read as a smudge, not a stone. Kept at low opacity so it is
   a texture behind the layout rather than a picture in front of it.
   Decorative only: no text ever sits on the lit facets. --------------------- */
.gem {
  position: absolute; inset: -25% -12% auto; height: 150%;
  z-index: 0; pointer-events: none;
  opacity: .15;
  filter: blur(26px) saturate(106%);
  /* Masked out of the middle: the facets belong at the edges of the frame, not
     across the headline. Keeps the refraction as peripheral atmosphere. */
  -webkit-mask-image: radial-gradient(115% 95% at 46% 46%, transparent 0%, transparent 34%, #000 82%);
          mask-image: radial-gradient(115% 95% at 46% 46%, transparent 0%, transparent 34%, #000 82%);
  background:
    conic-gradient(from 198deg at 74% 8%,
      var(--gf-a) 0deg 9deg, transparent 9deg 21deg,
      var(--gf-b) 21deg 27deg, transparent 27deg 44deg,
      var(--gf-a) 44deg 51deg, transparent 51deg 68deg,
      var(--gf-c) 68deg 77deg, transparent 77deg 96deg,
      var(--gf-a) 96deg 103deg, transparent 103deg 124deg,
      var(--gf-b) 124deg 132deg, transparent 132deg 360deg),
    conic-gradient(from 22deg at 14% 66%,
      var(--gf-a) 0deg 7deg, transparent 7deg 26deg,
      var(--gf-b) 26deg 34deg, transparent 34deg 55deg,
      var(--gf-a) 55deg 61deg, transparent 61deg 84deg,
      var(--gf-c) 84deg 93deg, transparent 93deg 360deg);
}
.hero .gem { height: 138%; }
/* on the charcoal CTA panel the same cut reads as light trapped inside the stone */
.cta-band .gem {
  /* flipped: on charcoal the facets have to be LIGHTER than the surface */
  --gf-a: #ffffff; --gf-b: #d8dee6; --gf-c: #aab2bd;
  inset: 0 0 auto; height: 100%; opacity: .26; filter: blur(32px) saturate(112%);
  -webkit-mask-image: radial-gradient(110% 100% at 50% 50%, transparent 0%, transparent 26%, #000 76%);
          mask-image: radial-gradient(110% 100% at 50% 50%, transparent 0%, transparent 26%, #000 76%);
}
.hero > .container { position: relative; z-index: 1; }
.hero-copy { position: relative; }

/* THE META ROW. The pill and the eyebrow were stacked: 44 + 26 + 27 + 14 = 111px
   and two lines of text before the promise, both of which say "Melbourne". They are
   the same KIND of thing — orientation, not argument — so they are now one row and
   one glance. align-items: center because the pill is a 44px object and the eyebrow
   a 24px line; baseline would hang the pill. flex-wrap means that below ~1274px the
   eyebrow drops under the pill by itself, with no media query. */
.hero-meta {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 10px 20px;
  margin-bottom: 20px;
}
.hero-meta .status-pill { margin-bottom: 0; }

/* The eyebrow was 18px/300 — the same size AND weight as the lead beneath it and as
   body copy, separated only by 1.45 units of contrast (6.04 vs 7.49:1). That is not
   a label, that is a first sentence. At 16px it matches the pill it now sits beside,
   so the row reads as one piece of furniture. Colour untouched: still 6.04:1. The ch
   cap is removed because it wrapped 16px type to two lines and un-levelled the row. */
.hero .eyebrow { font-size: 16px; line-height: 24px; margin-bottom: 0; max-width: none; }

.status-pill {
  display: inline-flex; align-items: center; gap: 9px;
  border: 1px solid var(--hairline); border-radius: var(--r-round);
  padding: 9px 18px; font-size: 16px; font-weight: 300; color: var(--muted);
  background: var(--card); margin-bottom: 26px;
}
/* "we are open" IS the live state — said in the family's words */
.status-pill .dot::after { opacity: 1; }

/* 52px capped from a 1444px viewport up, against a 42px UPPERCASE h2, is a ratio of
   1.24 — and uppercase carries more apparent mass per line, so the headline was
   barely louder than the section headings below it. 60px takes the ratio to 1.43.
   4.4vw (not 3.6) keeps it growing to the cap instead of stalling early.
   line-height 1.04: 1.1 is body-copy leading and it opened a visible river between
   two 60px lines. max-width 34ch keeps the natural break long-over-short. */
.hero-title {
  font-size: clamp(38px, 4.4vw, 60px);
  font-weight: 600; line-height: 1.04;
  letter-spacing: -0.012em;
  max-width: 19ch; margin: 0 0 32px;
}

.hero-cta { display: flex; flex-wrap: wrap; gap: 14px; }

.trust-strip { display: flex; flex-wrap: wrap; gap: 10px; padding-top: 28px; margin-bottom: 44px; border-top: 1px solid var(--hairline); }
.trust-strip li {
  display: inline-flex; align-items: center; gap: 9px;
  font-size: 16px; font-weight: 300; color: var(--tertiary);
  border: 1px solid var(--hairline); border-radius: var(--r-round);
  padding: 8px 18px; background: var(--card);
  transition: color .28s linear, border-color .28s linear;
}
.trust-strip li:hover { color: var(--text); border-color: #aeb6c1; }
.trust-strip li:hover .ts-dot { transform: scale(var(--dot-pop)); }
.trust-strip li:hover .ts-dot::after { opacity: 1; }
/* These are <li>, not controls — the boundary deliberately stays --hairline; a
   3:1 edge would make static decoration look clickable. Only the mechanics change:
   the padding shift was NOT in the transition list, so the content snapped 2px
   while the colour eased over .28s. The transform transition below was already
   declared and never driven by anything — this wires it, which also takes the
   shift off the layout path. */
.trust-strip li > *:not(.ts-dot) { transition: transform .28s var(--e-out); }
.trust-strip li:hover > *:not(.ts-dot) { transform: translateX(2px); }

/* v19: the hero system preview (.flow-card / .flow-rail / .flow-node / .tag) is
   GONE — its argument was rotated 90 degrees and became THE THREAD. See the
   v19 HERO block at the end of this file. .trust-strip's base rules above stay:
   eight interior pages use them inside .page-head. */

/* =============================================================================
   3. RECOGNITION
   ========================================================================== */
.recog-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--grid-gap); }
.recog-item {
  position: relative; overflow: hidden;
  display: flex; gap: 14px; align-items: flex-start;
  background: var(--card); border: 1px solid var(--hairline);
  border-radius: var(--r-card); padding: 30px 32px;
  transition: transform .3s var(--e-back), border-color .3s linear, box-shadow .3s var(--e-out);
}
.recog-item::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 90% at 0% 0%, rgba(61, 68, 78, .07), transparent 62%);
  opacity: 0; transition: opacity .3s linear;
}
.recog-item p { font-size: 18px; font-weight: 300; line-height: 1.5; transition: transform .3s var(--e-out); }
.recog-item:hover { transform: translateY(-6px); border-color: #aeb6c1; box-shadow: 0 16px 34px -20px rgba(20, 30, 70, .3); }
.recog-item:hover::after { opacity: 1; }
.recog-item:hover p { transform: translateY(-2px); }
.recog-item:hover .tick { transform: scale(1.12); }

/* Tier B — a mark disc: a 22px token that CARRIES A GLYPH. Shares the family's
   radius, transition and pop; the size differs because the job differs. */
.tick, .cmp-x {
  flex: none; position: relative;
  width: var(--mark-md); height: var(--mark-md);
  border-radius: var(--r-round);
  transition: transform .3s var(--e-back), background-color .3s linear;
}
.tick { margin-top: 3px; background: rgba(61, 68, 78, .12); }
/* 6x10 at 1.8px = 28.8 stroke-px, matching the cross's 2 x 9 at 1.6px = 28.8.
   Rendered in greyscale the old 5x9 at 1.6px carried 29% LESS ink than the x,
   so the "bad" mark visually outweighed the "good" one. Equalised — and stroke
   mass then becomes a second non-colour channel on top of the shape. */
.tick::after {
  content: ''; position: absolute; left: 6.5px; top: 4px;
  width: 6px; height: 10px;
  border: solid var(--accent); border-width: 0 1.8px 1.8px 0;
  transform: rotate(45deg);
}

.recog-close { margin-top: 34px; max-width: 76ch; font-size: 18px; font-weight: 300; color: var(--muted); }
.recog-close strong { font-weight: 600; color: var(--text); }

/* =============================================================================
   4. GROWTH SYSTEM — sticky, scroll-scrubbed five-stage sequence
   ========================================================================== */
/* v19: .sys-sec padding, .sys-scroll track and .sys-sticky pin are redefined in the
   v19 INK BAND block at the end of this file — the track is now a .js-enabled
   enhancement, so JS-off can no longer pin an overflowing box for 260vh. */
/* The rail column is sized to its content plus breathing room, not to a share of
   the grid. At 1.08fr/.92fr it was 541px wide holding ~130px of ink — 476px of
   void that read as a column that failed to load — while the h2 beside it broke
   to three lines. */
/* 208, not 292. The rail's labels only need 152.7px ("Follow up" + 38px pad);
   the 292 was set by .sys-done's single 274.1px line, which is opacity: 0 until
   the pin is 90% done — so for 90% of the scroll the reader saw 139.3px of void
   (47.7% of the column) beside every label. Measured after: 55.3px. */
/* v19: .sys-inner is redefined in the INK BAND block — one column by default,
   two only when .js-enabled + >=1024 + no-preference, matching isDesktopMotion(). */

/* The section heading rides in the sticky column beside the changing stage. */
.sys-head { margin-bottom: 40px; padding-bottom: 30px; }
.sys-head h2 { font-size: clamp(34px, 3.6vw, 52px); line-height: 1.06; max-width: 17em; }
.sys-lead { margin-top: 14px; max-width: 50ch; font-size: 19px; }

/* Tabular figures so "/ 05" cannot shift sideways when the stage number changes,
   and the live number in --text (13.4:1) rather than silver. */
.sys-count { font-size: 15px; font-weight: 400; letter-spacing: .1em; color: var(--accent-mid); margin-bottom: 14px; font-variant-numeric: tabular-nums; }
.sys-count #sysNum { color: var(--text); font-weight: 600; }
.sys-of { color: var(--tertiary); }

/* 268px was ~100px taller than any real pane, which lifted the whole block and
   left a hole under the tags. Tallest measured: 167px at 1600, 210px at 1024
   (where the tags wrap to two rows). 216px clears both. */
.sys-panes { position: relative; min-height: 210px; }
.sys-pane h3 { font-size: clamp(28px, 2.4vw, 34px); font-weight: 800; line-height: .95; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 14px; }
/* the promise line and the tags were 20/300/--muted vs 17/300/--muted — two of
   three signals identical. Separated by colour alone: 6.91 -> 13.36:1. */
.sys-pane > p { font-size: 20px; font-weight: 300; color: var(--text); max-width: 46ch; margin-bottom: 26px; }
/* DETERMINISTIC WRAPPING. Convert needs 829.1px for one row and 1280px can never
   offer more than 785, so a single row is unreachable at laptop widths — the fix
   is to make every stage wrap the SAME way instead of only one of them. The safe
   window is 466.7px (below it Convert breaks to three rows) to 590.0px (above it
   Improve fits one row and becomes the odd one out); 528 is the midpoint, +/-11.7%,
   which survives font-metric drift. Above 1400 there is room for all five on one
   row, so the cap is lifted there instead. */
.sys-tags { display: flex; flex-wrap: wrap; gap: 10px; max-width: 528px; }
.sys-tags li {
  font-size: 17px; font-weight: 300; color: var(--muted);
  border: 1px solid var(--edge); border-radius: var(--r-round);
  padding: 10px 22px; background: var(--card);
  transition: transform .3s var(--e-out), border-color .3s linear, color .3s linear;
}
.sys-pane.is-active .sys-tags li:hover { transform: translateY(-3px); border-color: var(--accent); color: var(--accent); }

/* Panes stack only while JS drives them; with JS off they flow as five blocks. */
.js-enabled .sys-pane {
  position: absolute; inset: 0;
  opacity: 0; transform: translateY(14px); pointer-events: none;
  transition: opacity .45s var(--e-out), transform .45s var(--e-out);
}
.js-enabled .sys-pane.is-active {
  opacity: 1; transform: none; pointer-events: auto;
  transition: opacity .34s linear .1s, transform .5s var(--e-out) .06s;
}
.js-enabled .sys-pane.is-prev { transform: translateY(-14px); }

.sys-rail { position: relative; padding-left: 38px; }
.sys-line { position: absolute; left: 5px; top: 10px; bottom: 10px; width: 2px; background: var(--hairline); }
/* Was --grad-a -> --accent. --grad-a is #f8fafc: 1.07:1 on this section's
   #f1f2f4 band, so the COMPLETED part of the progress line was invisible and
   only its leading edge read as progress. Silver-grey -> steel: 5.58:1 at the
   top, 8.78:1 at the leading edge. */
.sys-line-fill { display: block; width: 100%; height: 0; background: linear-gradient(180deg, var(--accent-mid), var(--accent)); transition: height .18s linear; }
.sys-track { display: grid; gap: 26px; }
.sys-node { position: relative; display: flex; align-items: center; gap: 14px; }
.sn-dot {
  position: absolute; left: -38px; top: 50%; margin-top: -7px;
  /* opaque fill: the .sys-line rail passes UNDER this dot and must be masked */
  --dot-fill: var(--band);
  /* This rail sits on --band, not on white, and the family's "live = metal face"
     renders NEAR-WHITE at 14px there (measured 1.07:1) — it would make the
     active marker the palest thing on the rail while completed dots sat solid.
     So the recipe is kept and its VALUES are re-pointed: idle #868c96 = 3.02:1,
     done --accent-mid = 5.58:1, live --accent = 8.78:1. Same grammar, values
     that survive the surface. */
  --dot-line: #868c96;
}
.sn-lbl { font-size: clamp(20px, 2vw, 26px); font-weight: 600; color: var(--tertiary); transition: color .35s linear, transform .35s var(--e-out); }
.sys-node.is-done .sn-dot { --dot-fill: var(--accent-mid); --dot-line: var(--accent-mid); }
/* Completed stages read as "done, de-emphasised" via COLOUR, never opacity —
   text must always compute to opacity 1. */
.sys-node.is-done .sn-lbl { color: var(--muted); }
.sys-node.is-active .sn-dot {
  --dot-fill: var(--accent);
  transform: scale(1.18);
  /* the ring and the halo in one declaration — a repaint, no geometry moves */
  box-shadow: inset 0 0 0 var(--dot-ring) var(--accent), 0 0 0 6px rgba(61, 68, 78, .13);
}
.sys-node.is-active .sn-lbl { color: var(--text); }
.sys-node:hover .sn-lbl { color: var(--accent); transform: translateX(3px); }
.sys-node:hover .sn-dot { --dot-line: var(--accent); }

/* Tag cascade — TRANSFORM ONLY. The tags never get their own opacity, so this
   adds ZERO new hidden-text surface: their visibility is entirely the pane's.
   Desktop-gated because below 1024px the scroll engine never sets is-active. */
@media (min-width: 1024px) {
  .js-enabled .sys-tags li {
    transform: translateY(12px);
    transition: transform .46s var(--e-out), border-color .3s linear, color .3s linear;
  }
  .js-enabled .sys-pane.is-active .sys-tags li { transform: none; }
  .js-enabled .sys-pane.is-active .sys-tags li:nth-child(1) { transition-delay: .12s; }
  .js-enabled .sys-pane.is-active .sys-tags li:nth-child(2) { transition-delay: .18s; }
  .js-enabled .sys-pane.is-active .sys-tags li:nth-child(3) { transition-delay: .24s; }
  .js-enabled .sys-pane.is-active .sys-tags li:nth-child(4) { transition-delay: .30s; }
  /* (0,5,1) beats the rule above (0,4,1), so hover always wins */
  .js-enabled .sys-pane.is-active .sys-tags li:hover { transform: translateY(-3px); transition-delay: 0s; }
}

@media (min-width: 1400px) {
  .sys-tags  { max-width: none; }
  .sys-panes { min-height: 160px; }
}

.sys-done { margin-top: 34px; padding-top: 22px; border-top: 1px solid var(--hairline); font-size: 20px; font-weight: 600; color: var(--accent); }
.js-enabled .sys-done { opacity: 0; transform: translateY(8px); transition: opacity .5s var(--e-out), transform .5s var(--e-out); }
.js-enabled .sys-scroll.is-complete .sys-done { opacity: 1; transform: none; }

/* =============================================================================
   5. SELECTED CLIENT WORK
   ========================================================================== */
.case-lead { background: var(--card); border: 1px solid var(--hairline); border-radius: var(--r-card); padding: 44px; }
.case-lead-top { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 18px; padding-bottom: 28px; margin-bottom: 30px; border-bottom: 1px solid var(--hairline); }
.case-lead-top h3 { font-size: 30px; font-weight: 600; }
.case-meta { font-size: 16px; font-weight: 300; color: var(--tertiary); }

.case-cols { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--grid-gap); }
.case-col .k { font-size: 16px; font-weight: 600; color: var(--accent-mid); margin-bottom: 10px; }
.case-col p { font-size: 17px; font-weight: 300; line-height: 1.5; color: var(--muted); }

.case-cta {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 18px; margin: var(--grid-gap) 0; padding: 26px 34px;
  border: 1px dashed var(--hairline); border-radius: var(--r-card);
}
.case-cta p { font-size: 17px; font-weight: 300; color: var(--muted); }

.case-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--grid-gap); }
.case-mini { background: var(--card); border: 1px solid var(--hairline); border-radius: var(--r-card); padding: 40px; }
.case-sector { font-size: 16px; font-weight: 300; color: var(--accent-mid); margin-bottom: 8px; transition: color .3s linear; }
.case-mini h4 { font-size: 26px; font-weight: 600; margin-bottom: 24px; transition: transform .4s var(--e-out); }
.case-line { padding: 16px 0; border-top: 1px solid var(--hairline); }
.case-line .l { display: block; font-size: 15px; font-weight: 400; color: var(--tertiary); margin-bottom: 6px; }
.case-line .case-body { font-size: 17px; font-weight: 300; line-height: 1.5; color: var(--muted); }
.case-mini:hover h4 { transform: translateX(6px); }

/* =============================================================================
   7. FIX / REBUILD / GROW — sticky card stack
   ========================================================================== */
.cur-toggle {
  display: inline-flex; gap: 4px; margin-top: 22px; padding: 4px;
  border: 1px solid var(--edge); border-radius: var(--r-round); background: var(--card);
}
.cur-btn {
  font: inherit; font-size: 15px; font-weight: 400;
  padding: 8px 18px; border: 0; border-radius: var(--r-round);
  background: transparent; color: var(--tertiary); cursor: pointer;
  transition: background-color .25s linear, color .25s linear,
              transform .25s var(--e-back);
}
/* the unselected segment had no hover state at all — the one genuinely dead
   control on the page. Wash keeps the label at 13.35:1. */
.cur-btn:hover { background: rgba(61, 68, 78, .07); color: var(--text); }
/* scale(.96) for the same reason as .burger: .985 on a ~64px segment is 1px. */
.cur-btn:active { transform: scale(.96); transition-duration: .12s; }
/* The flat --accent fill STAYS. Metal would be the "consistent" answer
   (.pnum.is-on and .pc-badge both use var(--grad) for selected) and was rejected
   on measurement: a metal segment against the white track is 1.42:1, the flat
   fill is 9.83:1. On a segmented control the selected state IS the information. */
.cur-btn.is-on { background: var(--accent); color: #fff; }
.cur-btn.is-on:hover { background: var(--accent-dk); color: #fff; }

.price-stack { padding-bottom: 4vh; }
/* Holds the LAST card (Grow) pinned for a beat instead of letting it scroll
   straight past. Must be an in-flow box, not parent padding — sticky is
   constrained by the containing block's CONTENT box. */
.price-tail { height: 46vh; }
.price-slot { position: sticky; top: 132px; }
.price-slot + .price-slot { margin-top: 52vh; }

.price-card {
  position: relative;
  max-width: 1020px; margin: 0 auto;
  display: grid; grid-template-columns: 1.35fr .85fr; gap: 52px;
  background: var(--card); border: 1px solid var(--hairline);
  border-radius: var(--r-card); padding: 44px 48px;
  box-shadow: 0 24px 60px -40px rgba(20, 30, 70, .5);
  transform-origin: center top;
  transition: transform .45s var(--e-out), border-color .3s linear, box-shadow .4s var(--e-out);
}
.price-card.is-featured { border-color: var(--accent); }
/* ---------- cursor spotlight -------------------------------------------------
   A soft cobalt highlight that follows the pointer inside every card surface.
   Purely decorative: it is opacity on a pseudo-element, never on text, and it
   is skipped entirely on touch and under reduced motion. --------------------- */
.case-lead, .case-mini, .price-card, .review, .team-panel, .quote-block { position: relative; }
.case-lead::after, .case-mini::after,
.price-card::after, .review::after, .team-panel::after, .quote-block::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; opacity: 0;
  transition: opacity .4s linear;
  background: radial-gradient(280px circle at var(--mx, 50%) var(--my, 50%),
              rgba(61, 68, 78, .09), transparent 62%);
}
.case-lead:hover::after, .case-mini:hover::after,
.price-card:hover::after, .review:hover::after, .team-panel:hover::after,
.quote-block:hover::after { opacity: 1; }

/* Covered-card veil for the sticky stack. Darkens the card WITHOUT touching the
   opacity of any text node — text always computes to opacity 1. */
.price-card::before {
  content: ''; position: absolute; inset: 0; z-index: 3;
  border-radius: inherit; pointer-events: none;
  background: #0e1118; opacity: var(--cover, 0);
}
.price-card:hover { border-color: #aeb6c1; box-shadow: 0 28px 64px -34px rgba(20, 30, 70, .42); }
.price-card.is-featured:hover { border-color: var(--accent); }

.pc-top { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.pc-top h3 { font-size: 34px; font-weight: 700; text-transform: uppercase; }
.pc-badge { font-size: 14px; font-weight: 500; color: var(--on-metal); background: var(--grad); box-shadow: var(--metal-inset), 0 1px 3px rgba(20, 22, 26, .22); border: 1px solid var(--metal-edge); border-radius: var(--r-round); padding: 6px 14px; }
.pc-for { font-size: 17px; font-weight: 300; color: var(--muted); margin-bottom: 24px; }
.pc-list { display: grid; }
.pc-list li {
  font-size: 17px; font-weight: 300; color: var(--muted);
  padding: 12px 0 12px 20px; border-top: 1px solid var(--hairline);
  position: relative; transition: transform .3s var(--e-out), color .3s linear;
}
/* Chevron drawn with borders, not a glyph, so assistive tech has nothing to read. */
.pc-list li::before {
  content: ''; position: absolute; left: 1px; top: 21px;
  width: 5px; height: 5px;
  border: solid var(--accent); border-width: 1.5px 1.5px 0 0;
  transform: rotate(45deg);
}
.price-card:hover .pc-list li { transform: translateX(3px); color: var(--text); }

.pc-side { display: flex; flex-direction: column; justify-content: flex-end; gap: 22px; }
.pc-price { text-align: right; }
.pc-from { display: block; font-size: 15px; font-weight: 300; color: var(--tertiary); margin-bottom: 8px; }
.pc-amt { display: block; font-size: 44px; font-weight: 800; line-height: 1; color: var(--accent); transition: color .3s linear; }
.pc-per { font-size: 18px; font-weight: 300; color: var(--tertiary); }
.pc-alt { display: block; margin-top: 8px; font-size: 13px; font-weight: 300; color: var(--tertiary); }
.price-card:hover .pc-amt { color: var(--accent-dk); }

.price-note { margin-top: 34px; max-width: 82ch; font-size: 16px; font-weight: 300; color: var(--tertiary); }

/* =============================================================================
   8. ONE ACCOUNTABLE TEAM
   ========================================================================== */
.team-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 60px var(--grid-gap); align-items: start; }
.team-copy .lead { margin-top: 18px; max-width: 62ch; }
.team-cta { margin-top: 30px; }

.check-list { margin-top: 30px; display: grid; gap: 14px; }
.check-row { display: flex; gap: 14px; align-items: flex-start; transition: transform .3s var(--e-out); }
.check-row p { font-size: 18px; font-weight: 300; line-height: 1.5; color: var(--muted); transition: color .3s linear; }
.check-row:hover { transform: translateX(3px); }
.check-row:hover p { color: var(--text); }

.team-panel { background: var(--card); border: 1px solid var(--hairline); border-radius: var(--r-card); padding: 44px; position: relative; }
.tp-head { font-size: 16px; font-weight: 600; color: var(--accent-mid); text-transform: uppercase; margin-bottom: 24px; }

.tp-rows { position: relative; padding-left: 24px; }
.tp-line { position: absolute; left: 3px; top: 16px; bottom: 16px; width: 2px; background: var(--hairline); }
.tp-line-fill { display: block; width: 100%; height: 0; background: linear-gradient(180deg, var(--grad-a), var(--accent)); transition: height 1.1s var(--e-out) .25s; }
.tp-rows.is-on .tp-line-fill { height: 100%; }

.tp-row {
  position: relative; overflow: hidden;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 16px;
  padding: 20px 14px; border-top: 1px solid var(--hairline);
  transition: opacity .4s linear, padding .4s var(--e-out);
}
.tp-row::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(90deg, rgba(61, 68, 78, .10), rgba(61, 68, 78, 0));
  transform: scaleX(0); transform-origin: left center;
  transition: transform .45s var(--e-out);
}
.tp-k { position: relative; z-index: 1; font-size: 22px; font-weight: 600; min-width: 150px; transition: transform .4s var(--e-out), color .3s linear; }
.tp-d { position: relative; z-index: 1; font-size: 17px; font-weight: 300; color: var(--muted); transition: transform .4s var(--e-out), color .3s linear; }
.tp-arw {
  position: relative; z-index: 1; margin-left: auto;
  width: 18px; height: 18px; flex: none;
  border: solid var(--accent); border-width: 1.6px 1.6px 0 0;
  transform: rotate(45deg) translate3d(-6px, 6px, 0); opacity: 0;
  transition: transform .4s var(--e-out), opacity .3s linear;
}
/* was `.tp-rows:hover .tp-row { opacity: .62 }` — the last dim-text hover on
   the site, and it dimmed .tp-k and .tp-d, both real text. The recede now runs
   on the row's hairline only; every label keeps opacity 1 and its own colour,
   and the hovered row's text still goes DARKER (see .tp-k / .tp-d below). */
.tp-rows:hover .tp-row { border-color: var(--dot-mute); }
.tp-rows .tp-row:hover { padding-left: 22px; padding-right: 6px; }
.tp-row:hover::before { transform: scaleX(1); }
.tp-row:hover .tp-k { transform: translateX(8px); color: var(--on-metal); }
.tp-row:hover .tp-d { transform: translateX(-3px); color: var(--text); }
.tp-row:hover .tp-arw { transform: rotate(45deg) translate3d(0, 0, 0); opacity: 1; }

/* rotating circular badge */
.badge-wrap { position: relative; width: 144px; height: 144px; margin-top: 38px; }
.rot-badge { display: block; width: 144px; height: 144px; }
/* letter-spacing tuned so the string measures 335px against the 339px circular
   path — any longer and the text truncates mid-word. Re-measure if edited. */
.badge-text { font-size: 11px; font-weight: 600; fill: var(--accent); letter-spacing: 0.75px; }
.badge-ring { fill: none; stroke: var(--hairline); stroke-width: 1; }
/* the badge core and an active stage node are now literally the same object */
.badge-core { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); }
.badge-core::after { opacity: 1; }

/* =============================================================================
   9. TESTIMONIALS — static 3-up grid (three real reviews; no autoplay carousel)
   ========================================================================== */
.review-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--grid-gap); }
.review {
  margin: 0; position: relative;
  background: var(--card); border: 1px solid var(--hairline);
  border-radius: var(--r-card); padding: 40px;
  display: flex; flex-direction: column; justify-content: space-between; gap: 24px;
  transition: transform .4s var(--e-back), border-color .3s linear, box-shadow .4s var(--e-out);
}
.rv-mark { font-size: 56px; line-height: .6; font-weight: 700; color: var(--hairline); transition: color .3s linear; }
/* flex: 1 1 auto — the cards are all exactly the same height, but
   space-between was centring the middle child, so the three quotes started at
   99 / 124 / 139px and their attributions at 268 / 290 / 290px. Nothing aligned
   across three identically-sized cards. */
.review blockquote { flex: 1 1 auto; margin: 0; font-size: 20px; font-weight: 300; line-height: 1.45; color: var(--text); }
.review figcaption { display: grid; gap: 4px; padding-top: 24px; border-top: 1px solid var(--hairline); }
.rv-who  { font-size: 17px; font-weight: 600; }
.rv-role { font-size: 15px; font-weight: 300; color: var(--tertiary); }
.rv-src  { font-size: 15px; font-weight: 300; color: var(--accent-mid); transition: color .3s linear; }
.review:hover { transform: translateY(-4px); border-color: #aeb6c1; box-shadow: 0 16px 36px -22px rgba(20, 30, 70, .3); }
.review:hover .rv-mark { color: var(--accent); }
.review:hover .rv-src { color: var(--accent-dk); }

/* =============================================================================
   10. FAQ
   ========================================================================== */
.faq-grid { display: grid; grid-template-columns: .8fr 1.2fr; gap: 72px; align-items: start; }
.faq-head { position: sticky; top: 140px; }
.faq-list { border-top: 1px solid var(--hairline); }
.faq-row { border-bottom: 1px solid var(--hairline); transition: border-color .3s linear; }
.faq-row.is-open { border-bottom-color: var(--accent); }
.faq-row h3 { margin: 0; font-weight: inherit; }
.faq-q {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 24px;
  padding: 26px 0; background: none; border: 0; text-align: left; cursor: pointer;
  font: inherit; font-size: 22px; font-weight: 600; color: var(--text);
  transition: padding-left .32s var(--e-out), color .3s linear;
}
.faq-q:hover, .faq-q:focus-visible { padding-left: 6px; color: var(--on-metal); }
/* the row is a <button> and had no press at all */
.faq-q:active { color: #000; transition-duration: .12s; }
.faq-ico {
  position: relative; flex: none; width: 40px; height: 40px;
  border-radius: var(--r-round); border: 1px solid var(--edge); background: transparent;
  transition: transform .4s var(--e-out), background-color .3s linear, border-color .3s linear;
}
.faq-ico::before, .faq-ico::after {
  content: ''; position: absolute; left: 50%; top: 50%; background: currentColor;
  transition: opacity .3s linear;
}
.faq-ico::before { width: 13px; height: 1.6px; margin: -.8px 0 0 -6.5px; }
.faq-ico::after  { width: 1.6px; height: 13px; margin: -6.5px 0 0 -.8px; }
/* keyboard got the row treatment but not the icon treatment — now both */
.faq-q:hover .faq-ico, .faq-q:focus-visible .faq-ico { background: rgba(61, 68, 78, .09); border-color: var(--accent); }
/* deepen rather than move: .faq-row.is-open already owns .faq-ico's transform */
.faq-q:active .faq-ico { background: rgba(61, 68, 78, .18); transition-duration: .12s; }
.faq-row.is-open .faq-ico { transform: rotate(45deg); border-color: var(--accent); color: var(--accent); }

.faq-a { overflow: hidden; }
.js-enabled .faq-a { height: 0; }
.faq-a-inner { padding: 0 60px 30px 0; }
.faq-a-inner p { font-size: 17px; font-weight: 300; line-height: 1.55; color: var(--muted); }

/* =============================================================================
   11. GROWTH REVIEW / CONTACT — dark panel that widens as it enters
   ========================================================================== */
.cta-band { position: relative; margin-top: 80px; padding: 110px 0 100px; color: #fff; }
.cta-panel-bg {
  position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(160deg, #1b2030 0%, var(--ink) 55%, #101219 100%);
  border-radius: var(--r-card) var(--r-card) 0 0;
  /* Bottom origin: the panel's bottom edge is welded to the footer (its #101219
     stop against the footer's --ink #131519 — the seam is invisible), so scaling
     from the centre would open a light gap underneath. From the bottom the box
     grows UP out of the footer and only the top edge travels. */
  transform-origin: 50% 100%;
}
/* The closed state is JS-ONLY. It used to be an ungated `transform: scaleX(.92)`,
   which with JS disabled left the panel permanently 8% narrow — measured 3px
   clear of the white headline at 390px, i.e. one padding tweak from white text
   on a white page. */
.js-enabled .cta-panel-bg { transform: scale(.915, .938); }
/* Below 1024px the gutter (--pad 24/18px) is smaller than any useful inset, so
   the panel does not breathe at all — which removes that 3px near-miss. */
@media (max-width: 1023px) { .cta-panel-bg { transform: none !important; } }
.cta-band > .container { position: relative; z-index: 1; }
.cta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 60px var(--grid-gap); align-items: start; }
.cta-copy .lead { margin-top: 18px; max-width: 60ch; color: rgba(255,255,255,.72); }
.cta-band h2 { color: #fff; }
.cta-band .eyebrow { color: var(--metal-lite); }
.cta-band .check-row p { color: rgba(255,255,255,.76); }
.cta-band .check-row:hover p { color: #fff; }
.cta-band .tick { background: rgba(216, 222, 230, .20); }
.cta-band .tick::after { border-color: var(--metal-lite); }

.form-wrap { position: relative; }
.qform { background: var(--card); border: 1px solid rgba(255,255,255,.14); border-radius: var(--r-card); padding: 40px; display: grid; gap: 18px; color: var(--text); }
.fstep { display: grid; gap: 18px; }
.js-enabled .qform[data-step="1"] .fstep-2 { display: none; }
.js-enabled .qform[data-step="2"] .fstep-next { display: none; }
.fstep-next { display: none; }
.js-enabled .fstep-next { display: inline-flex; }

.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.field { display: grid; gap: 8px; }
.field label { font-size: 15px; font-weight: 400; color: var(--muted); transition: color .25s linear; }
.field .opt { color: var(--tertiary); font-weight: 300; }
.field input, .field textarea, .field select {
  font-family: inherit; font-size: 17px; font-weight: 300; color: var(--text);
  background: var(--bg);
  border: 1px solid var(--edge); border-radius: 18px;    /* 3.36 / 3.47 */
  padding: 14px 18px; width: 100%;
  transition: border-color .25s linear, box-shadow .25s linear;
}
.field textarea { min-height: 104px; resize: vertical; }
/* HOVER DARKENS. It used to LIGHTEN to #a9aeb6 — 2.16:1 — so pointing at a field
   pushed its boundary BELOW the 3:1 that identifies it as a control. Every other
   hover on this site adds definition; this one removed it. 3.36 -> 4.56. */
.field input:hover, .field textarea:hover, .field select:hover { border-color: var(--edge-on); }
.field input:focus, .field textarea:focus, .field select:focus {
  border-color: var(--accent); outline: none;
  box-shadow: 0 0 0 4px rgba(61, 68, 78, .16);
}
/* The quiet border+halo above is right for a mouse click, but `outline: none` on
   :focus also removed the global ring for KEYBOARD users (0-2-1 beats 0-1-0).
   Keyboard focus now gets the identical ring every other control has. */
.field input:focus-visible, .field textarea:focus-visible, .field select:focus-visible {
  outline: 2px solid var(--focus); outline-offset: 3px;
}
.field:focus-within label { color: var(--text); }
.field input::placeholder, .field textarea::placeholder { color: var(--tertiary); }
/* was #9aa2ad = 2.49:1 — a field became HARDER to see by validating. Now 4.56:1. */
.field.is-valid input, .field.is-valid textarea { border-color: var(--edge-on); }

.form-msg { font-size: 16px; font-weight: 300; color: var(--muted); min-height: 1px; }
.form-msg.ok  { color: var(--accent); }
.form-msg.err { color: #b3261e; }
.form-done { text-align: center; padding: 30px 10px; }
.form-done h3 { font-size: 24px; margin-bottom: 12px; }
.form-done p { font-size: 17px; font-weight: 300; color: var(--muted); }

/* =============================================================================
   FOOTER
   ========================================================================== */
.footer { background: var(--ink); color: rgba(255,255,255,.72); padding: 72px 0 40px; }
.footer .brand { color: #fff; }
.foot-grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1.2fr; gap: var(--grid-gap); }
.foot-brand p { margin-top: 16px; max-width: 40ch; font-size: 16px; font-weight: 300; }
.foot-social { display: flex; gap: 10px; margin-top: 20px; }
.foot-social a {
  width: 40px; height: 40px; border-radius: var(--r-round);
  border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.04);
  display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,.75);
  transition: color .25s linear, border-color .25s linear, transform .3s var(--e-out);
}
.foot-social a:hover, .foot-social a:focus-visible { color: #fff; border-color: rgba(255,255,255,.55); transform: translateY(-2px); }
.foot-social a:active { transform: translateY(0); transition-duration: .12s; }

.foot-col h5 { font-size: 16px; font-weight: 600; text-transform: uppercase; margin-bottom: 16px; color: #fff; }
.foot-col li { margin-bottom: 10px; font-size: 16px; font-weight: 300; }
.foot-col a { transition: transform .3s var(--e-out), color .25s linear; display: inline-block; }
.foot-col a:hover, .foot-col a:focus-visible { color: #fff; transform: translateX(3px); }
.foot-col a:active { transform: translateX(0); transition-duration: .12s; }

/* Registered so --glow can be TRANSITIONED. Only the fade uses it; the spot's
   position comes from --mx/--my and tracks the pointer with no lag, which is
   what "followed" means. Unsupported browsers just snap the fade on and off. */
@property --glow { syntax: '<number>'; inherits: false; initial-value: 0; }

.foot-word {
  margin: calc(64px - .085em) 0 calc(64px - .10em); overflow: hidden;
  /* CENTRED — the owner's instinct, and the measurement agrees. "AUSTRA DIGITAL"
     needs 8.48em of measure and the container gives at most 6.57em, so this has
     always been a TWO-LINE LOCKUP at every viewport from 390 to 2560, and the two
     lines are within 5% of each other in width. That is a near-square MARK, not a
     heading — and a mark centres. Left-aligned it left 438px (35%) of the footer
     empty on the right, under a CONTACT column that fills it.
     text-align centres the ADVANCE box, not the ink: -.02em tracking leaves
     non-ink trailing space and the sidebearings are unequal (A opens at 1px, D at
     13px), so the ink landed +2.03px / +6.71px right of centre — a 13.4px error on
     line two. box-sizing is border-box, so padding-right moves the centring axis
     left by HALF its value: .04em corrects to a 5.8px residual on a 1248px
     measure (0.47%). The em margins correct the vertical the same way — line-height
     .9 crops the leading, so a flat 56/40 read as 72/59; this measures optical
     64px above and below at every viewport.
     The glow still tracks: --mx/--my map to the padding box, which stays 1248px
     either way, and the no-pointer 50% 50% fallback now lands dead centre of the
     mark instead of inside "AUSTRA". */
  /* SPLIT TO BOTH EDGES — Austra left, Digital right. This is the answer to the
     centre-vs-fill argument: "AUSTRA DIGITAL" needs 8.48em against a 6.57em
     container, so it was always going to be two lines. Centred, it balanced the
     void but abandoned the x=169 rail every other section holds. Justified to the
     edges it FILLS the measure and anchors BOTH sides — and it stops being a
     heading that happens to wrap and becomes a designed lockup.
     No optical padding needed: each word is flushed to a real edge rather than
     centred on an advance box, so the sidebearing error has nowhere to land. */
  /* Two lines by necessity (8.48em of measure into a 6.57em container), so each
     word gets its own line and its own edge: AUSTRA flush left on the x=169 rail,
     DIGITAL flush right on the container edge. Flex + space-between does NOT do
     this — once the words wrap, each flex LINE justifies independently and both
     land left. Block lines with the second right-aligned is the only thing that
     actually reaches both edges. */
  display: block;
  font-size: clamp(46px, 12vw, 190px); line-height: .9;
  font-weight: 800; text-transform: uppercase; letter-spacing: -.02em;
  color: transparent;
  /* Two layers, both clipped to the glyphs. The base gradient is UNCHANGED, and
     the radial only ever ADDS white — at --glow: 0 its alpha is 0 and every
     pixel is byte-identical to the old rest state. The wordmark can never get
     darker than it was. */
  background-image:
    radial-gradient(420px circle at var(--mx, 50%) var(--my, 50%),
      rgba(255, 255, 255, calc(.40 * var(--glow, 0))) 0%,
      rgba(255, 255, 255, calc(.15 * var(--glow, 0))) 45%,
      rgba(255, 255, 255, 0) 74%),
    linear-gradient(100deg, rgba(255,255,255,.16), rgba(255,255,255,.07) 46%, rgba(255,255,255,.24));
  -webkit-background-clip: text; background-clip: text;
  /* Only the custom property animates. Transitioning `background` would smear
     the spot 600ms behind the pointer — the opposite of following it. */
  transition: --glow .5s linear;
}
.foot-word > span { display: block; }
.foot-word > span:last-child { text-align: right; }
.foot-word:hover { --glow: 1; }

.foot-bar {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px;
  margin-top: 40px; padding-top: 26px; border-top: 1px solid rgba(255,255,255,.14);
  font-size: 15px; font-weight: 300; color: rgba(255,255,255,.55);
}

/* underline sweep on nav + footer links */
.nav-links a, .foot-col a, .drawer a:not(.btn) { position: relative; }
.nav-links a::after, .foot-col a::after, .drawer a:not(.btn)::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -3px; height: 1px;
  background: currentColor;
  transform: scaleX(0); transform-origin: left center;
  transition: transform .32s var(--e-out);
}
.nav-links a:hover::after, .nav-links a:focus-visible::after, .nav-links a.is-active::after,
.foot-col a:hover::after, .foot-col a:focus-visible::after,
.drawer a:not(.btn):hover::after,
.drawer a:not(.btn):focus-visible::after { transform: scaleX(1); }

/* =============================================================================
   MOTION LAYER — pre-animation hidden states (JS-gated)
   ========================================================================== */
.js-enabled [data-reveal]:not([data-revealed]) { opacity: 0; transform: translateY(24px); }
.js-enabled [data-reveal-lines]:not([data-revealed]) { opacity: 0; }
.js-enabled [data-hero-step]:not([data-revealed]):not([data-reveal-lines]) { opacity: 0; transform: translateY(14px); }

/* line-mask scaffolding for heading reveals — unwrapped again once revealed,
   so reflow on resize can never clip a line. */
.line-mask { display: block; overflow: hidden; padding-bottom: .09em; margin-bottom: -.09em; }
.line-inner { display: block; will-change: transform; }

/* hover: card lift */
.case-mini, .case-lead {
  transition: transform .45s var(--e-back), box-shadow .45s var(--e-out), border-color .25s linear;
}
.case-mini:hover, .case-lead:hover {
  transform: translateY(-5px);
  box-shadow: 0 18px 40px -22px rgba(20, 30, 70, .3);
  border-color: #aeb6c1;
}
.case-mini:hover .case-sector { color: var(--accent-dk); }

.hero-bg { will-change: transform; }

/* Net #5 — the head script un-gates everything if the module never signals ready. */
.js-fallback [data-reveal]:not([data-revealed]),
.js-fallback [data-reveal-lines]:not([data-revealed]),
.js-fallback [data-hero-step]:not([data-revealed]),
.js-fallback .sys-done { opacity: 1 !important; transform: none !important; }
.js-fallback .sys-pane { position: static !important; opacity: 1 !important; transform: none !important; pointer-events: auto !important; }
.js-fallback .sys-tags li { transform: none !important; }
.js-fallback .faq-a { height: auto !important; }
.js-fallback .cta-panel-bg { transform: none !important; }
.js-fallback body { cursor: auto !important; }


/* =============================================================================
   v13 ADDITIONS — before/after comparison, case links, radio choices,
   sticky mobile CTA. Placed BEFORE the responsive + reduced-motion blocks so
   those still win.
   ========================================================================== */

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------- before / after comparison ---------- */
.compare { border-top: 1px solid var(--hairline); }
.cmp-head { display: grid; grid-template-columns: 1fr 1fr; gap: var(--grid-gap); padding: 18px 18px 14px; }
.cmp-h { font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
.cmp-h-old { color: var(--tertiary); }
.cmp-h-new { color: var(--text); }

.cmp-rows { display: grid; }
/* row-gap: 0 is load-bearing. `gap` was set on BOTH axes though it was only ever
   the column gutter; with one grid row it never showed. The label adds a second
   row, and the inherited 20px then costs 120px of page height across six rows —
   measured 122px per row with it, 102px without. */
.cmp-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--grid-gap);
  border-top: 1px solid var(--hairline);
  transition: background-color .3s linear;
}
.cmp-row:hover { background: rgba(61, 68, 78, .035); }
/* One line per cell, so the x and the tick share a baseline and the row reads
   ACROSS again. The left cell is now an h3 — it carries the symptom in the
   reader's words, which is both the heading and the content, so there is nothing
   left to duplicate underneath it. */
.cmp-old, .cmp-new {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 18px; font-size: 17px; line-height: 1.5;
  transition: transform .35s var(--e-out), color .3s linear;
}
.cmp-old { font-weight: 600; font-size: 18px; }
.cmp-new { font-weight: 300; }
.cmp-old { color: var(--tertiary); }
.cmp-new { color: var(--text); background: var(--card); border-radius: 20px; }
/* Both marks respond; only the connected column moves. Before this, .tick had a
   hover pop and .cmp-x — its opposite number, 20px away in the same row — had
   no transition at all. */
.cmp-row:hover .cmp-new { transform: translateX(3px); }
.cmp-row:hover .cmp-new .tick { transform: scale(var(--mark-pop)); }
.cmp-row:hover .cmp-old .cmp-x { transform: scale(var(--mark-pop)); }

/* .cmp-label and .cmp-n retired. Once the label moved INTO the left cell the
   ordinal and the x were fighting for the same 24px gutter — and the x carries
   meaning (for a protanope the red and green measure 1.11:1, so the glyph IS the
   meaning) while the ordinal carried ceremony only. */

/* THE RED AND THE GREEN. Scoped to this component only: these are the only two
   hues on the site, and they exist where a red and a green are adjacent and
   opposed. The green is NOT applied to .tick globally — the tick is reused in
   the check-list and on the charcoal CTA band, where #1c5340 is 2.05:1 and
   would force a second green into existence. */
.cmp-x { margin-top: 2px; background: rgba(163, 69, 59, .10); }
.cmp-x::before, .cmp-x::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 9px; height: 1.6px; background: var(--mark-no); margin: -.8px 0 0 -4.5px;
}
.cmp-x::before { transform: rotate(45deg); }
.cmp-x::after  { transform: rotate(-45deg); }
.cmp-new .tick { background: rgba(28, 83, 64, .12); }
.cmp-new .tick::after { border-color: var(--mark-yes); }

/* ---------- case-study outbound links ---------- */
.case-link {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 15px; font-weight: 400; color: var(--accent);
  border-bottom: 1px solid transparent;
  transition: border-color .25s linear, color .25s linear;
}
.case-link:hover, .case-link:focus-visible { border-bottom-color: var(--accent); color: var(--accent-dk); }
.case-mini h4 { min-height: 2.4em; }
.case-slot { display: block; min-height: 24px; margin: -14px 0 18px; }
.case-mini .case-link { margin: 0; }

/* ---------- pricing scope note ---------- */
.pc-note {
  margin-top: 14px; text-align: right;
  font-size: 14px; font-weight: 300; line-height: 1.45; color: var(--tertiary);
}

/* ---------- form: step counter, radio choices, trust line ---------- */
.fstep-count {
  font-size: 12px; font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--accent-mid);
}
.choices { border: 0; padding: 0; margin: 0; min-width: 0; }
.choices legend { font-size: 15px; font-weight: 400; color: var(--muted); padding: 0; margin-bottom: 10px; }
.choice {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 13px 16px; margin-bottom: 8px;
  border: 1px solid var(--edge); border-radius: 16px;
  font-size: 16px; font-weight: 300; line-height: 1.4; cursor: pointer;
  transition: border-color .2s linear, background-color .2s linear,
              transform .2s var(--e-out);
}
.choice:last-child { margin-bottom: 0; }
/* same inversion as the inputs: hover was #a9aeb6 = 2.16:1. Now 3.36 -> 4.56. */
.choice:hover { border-color: var(--edge-on); }
.choice:active { transform: scale(.99); transition-duration: .12s; }
.choice input {
  appearance: none; -webkit-appearance: none;
  flex: none; width: 18px; height: 18px; margin: 1px 0 0;
  border: 1px solid var(--edge); border-radius: var(--r-round);
  background: var(--card);
  transition: border-color .2s linear, box-shadow .2s linear;
}
.choice input:checked { border-color: var(--accent); box-shadow: inset 0 0 0 4px var(--accent); }
.choice:hover input { border-color: var(--edge-on); }
.choice:has(input:checked) { border-color: var(--accent); background: rgba(61, 68, 78, .06); }
/* keyboard: the whole card shows where you are, not just the 18px dot — so
   arrowing through the group is legible at a glance. */
.choice:has(input:focus-visible) { border-color: var(--accent); background: rgba(61, 68, 78, .06); }
.form-trust {
  font-size: 14px; font-weight: 300; line-height: 1.5;
  color: var(--tertiary); text-align: center;
}

/* ---------- sticky mobile CTA ---------- */
.sticky-cta {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 95;
  display: none;
  padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
  transform: translateY(130%);
  visibility: hidden;
  transition: transform .38s var(--e-out), visibility 0s linear .38s;
}
.sticky-cta.is-on { transform: none; transition: transform .38s var(--e-out), visibility 0s; }
.sticky-cta[hidden] { display: none !important; }
.sticky-cta-in {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 10px 10px 10px 20px;
  background: rgba(252, 252, 252, .95);
  -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
  border: 1px solid var(--edge); border-radius: var(--r-round);
  box-shadow: 0 14px 34px -18px rgba(20, 30, 70, .45);
}
.sticky-cta-txt { display: grid; line-height: 1.25; min-width: 0; }
.sticky-cta-txt strong { font-size: 15px; font-weight: 600; }
.sticky-cta-txt span { font-size: 12px; color: var(--tertiary); line-height: 1.35; }
.sticky-cta .btn-sm { flex: none; white-space: nowrap; }


/* ---------- cursor: dot + lagging ring -------------------------------------
   Ported from the v1 build. The dot uses mix-blend-mode: difference, so it
   inverts whatever is under it — dark on the warm-white sections, light on the
   charcoal CTA band and footer — without any JS colour switching. The native
   cursor is hidden on desktop pointer devices only, and restored over text
   inputs so the I-beam is never lost. ------------------------------------- */
.cursor-dot, .cursor-ring {
  position: fixed; left: 0; top: 0; z-index: 300;
  border-radius: var(--r-round);
  pointer-events: none; opacity: 0;
  transform: translate3d(-50%, -50%, 0);
  will-change: transform;
}
.cursor-dot {
  width: 12px; height: 12px;
  background: #fff; mix-blend-mode: difference;
  transition: width .28s var(--e-out), height .28s var(--e-out), opacity .3s linear;
}
.cursor-ring {
  width: 38px; height: 38px; z-index: 299;
  border: 1px solid rgba(61, 68, 78, .55);
  transition: width .3s var(--e-out), height .3s var(--e-out),
              border-color .3s linear, opacity .3s linear;
}
.cursor-dot.is-on { opacity: 1; }
.cursor-ring.is-on { opacity: .9; }
.cursor-dot.is-link { width: 46px; height: 46px; }
.cursor-ring.is-link { width: 12px; height: 12px; border-color: rgba(61, 68, 78, .95); }

/* MUST be JS-gated: with JS disabled there is no replacement dot, so hiding the
   native cursor would leave the page with no pointer at all. */
@media (min-width: 1024px) and (hover: hover) and (pointer: fine) {
  .js-enabled body { cursor: none; }
  /* never take the I-beam away from something you type into */
  input, textarea, select, [contenteditable] { cursor: auto; }
}
@media (max-width: 1023px), (hover: none) {
  .cursor-dot, .cursor-ring { display: none !important; }
}


/* =============================================================================
   INTERIOR PAGES — case studies, about, client-work index
   ========================================================================== */
.page-head { position: relative; overflow: hidden; padding: 156px 0 var(--sec-gap); }
.page-head > .container { position: relative; z-index: 1; }
.crumb { font-size: 15px; font-weight: 300; color: var(--tertiary); margin-bottom: 22px; }
.crumb a { color: var(--muted); border-bottom: 1px solid transparent; transition: border-color .25s linear, color .25s linear; }
.crumb a:hover, .crumb a:focus-visible { color: var(--accent); border-bottom-color: var(--accent); }
.crumb span { margin: 0 8px; color: var(--hairline); }

.page-title {
  font-size: clamp(32px, 3.4vw, 50px); font-weight: 600; line-height: 1.12;
  max-width: 20ch; margin: 4px 0 24px;
}
.page-lead { font-size: 19px; max-width: 62ch; }
.page-note { margin-top: 20px; max-width: 62ch; font-size: 16px; font-weight: 300; color: var(--tertiary); }
.page-link { margin-top: 24px; font-size: 16px; }
.page-head .trust-strip { margin-top: 34px; margin-bottom: 0; }

/* key facts */
.fact-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--grid-gap); margin-bottom: 56px; }
.fact {
  display: grid; gap: 8px; align-content: start;
  background: var(--card); border: 1px solid var(--hairline);
  border-radius: 24px; padding: 24px 26px;
  transition: transform .35s var(--e-back), border-color .25s linear;
}
.fact:hover { transform: translateY(-4px); border-color: #aeb6c1; }
.fact-k { font-size: 13px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--accent-mid); }
.fact-v { font-size: 17px; font-weight: 300; line-height: 1.45; color: var(--text); }

/* article body */
.prose { max-width: 74ch; }
.prose h2 { font-size: clamp(26px, 2.4vw, 34px); line-height: 1.15; margin: 56px 0 18px; }
.prose h2:first-child { margin-top: 0; }
.prose p { font-size: 18px; font-weight: 300; line-height: 1.65; color: var(--muted); margin-bottom: 18px; }
.prose p strong, .prose li strong { color: var(--text); font-weight: 600; }
.prose em { font-style: italic; }
.prose-list { display: grid; gap: 14px; margin: 6px 0 20px; }
.prose-list li {
  position: relative; padding-left: 26px;
  font-size: 18px; font-weight: 300; line-height: 1.6; color: var(--muted);
}
.prose-list li::before {
  content: ''; position: absolute; left: 2px; top: 11px;
  width: 6px; height: 6px; border: solid var(--accent); border-width: 1.6px 1.6px 0 0;
  transform: rotate(45deg);
}
.prose-note {
  margin-top: 26px; padding: 22px 26px;
  background: var(--card); border: 1px solid var(--hairline); border-radius: 24px;
  font-size: 17px; color: var(--muted);
}

.quote-block {
  margin: 34px 0; padding: 32px 34px;
  background: var(--card); border: 1px solid var(--hairline); border-radius: var(--r-card);
  transition: transform .4s var(--e-back), border-color .25s linear;
}
.quote-block:hover { transform: translateY(-4px); border-color: #aeb6c1; }
.quote-block blockquote { margin: 0 0 18px; font-size: 20px; font-weight: 300; line-height: 1.5; color: var(--text); }
.quote-block figcaption { display: grid; gap: 4px; padding-top: 18px; border-top: 1px solid var(--hairline); }

/* client-work index cards */
.cs-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--grid-gap); }
.cs-card {
  position: relative; display: flex; flex-direction: column; gap: 14px;
  background: var(--card); border: 1px solid var(--hairline);
  border-radius: var(--r-card); padding: 40px;
  transition: transform .45s var(--e-back), border-color .25s linear, box-shadow .45s var(--e-out);
}
.cs-card::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; opacity: 0;
  transition: opacity .4s linear;
  background: radial-gradient(280px circle at var(--mx, 50%) var(--my, 50%), rgba(6, 95, 70, .08), transparent 62%);
}
.cs-card:hover { transform: translateY(-5px); border-color: #aeb6c1; box-shadow: 0 18px 40px -22px rgba(20, 30, 70, .3); }
.cs-card:hover::after { opacity: 1; }
.cs-sector { font-size: 15px; font-weight: 300; color: var(--accent-mid); }
.cs-card h2 { font-size: 28px; font-weight: 600; text-transform: none; line-height: 1.15; transition: transform .4s var(--e-out); }
.cs-card:hover h2 { transform: translateX(5px); }
.cs-body { font-size: 17px; font-weight: 300; line-height: 1.55; color: var(--muted); }
.cs-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.cs-tags li { font-size: 14px; font-weight: 300; color: var(--tertiary); border: 1px solid var(--hairline); border-radius: var(--r-round); padding: 6px 14px; }
.cs-go { margin-top: auto; padding-top: 18px; display: inline-flex; align-items: center; gap: 8px; font-size: 16px; color: var(--accent); }

/* prev / next */
.page-next {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px;
  margin-top: 48px; padding-top: 26px; border-top: 1px solid var(--hairline);
}
.page-next a {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 17px; color: var(--muted);
  transition: color .25s linear, transform .3s var(--e-out);
}
.page-next a:hover { color: var(--accent); transform: translateX(3px); }

.about-panel { margin: 24px 0 12px; }


/* =============================================================================
   SCREENSHOTS — real client sites in a light browser frame
   The frame is CSS, not baked into the JPEG, so it stays crisp and can be
   restyled without recapturing. Images are 1200x750 (2x the card slot).
   ========================================================================== */
.shot {
  margin: 0; position: relative;
  border: 1px solid var(--hairline); border-radius: 22px;
  background: var(--card); overflow: hidden;
  transition: border-color .3s linear, box-shadow .45s var(--e-out), transform .45s var(--e-back);
}
.shot-bar {
  display: flex; align-items: center; gap: 6px;
  padding: 11px 14px; border-bottom: 1px solid var(--hairline);
  background: linear-gradient(180deg, #fbfbfc, #f4f5f7);
}
.shot-bar i { width: 8px; height: 8px; border-radius: var(--r-round); background: #d8dade; display: block; }
.shot-bar em {
  margin-left: 10px; font-style: normal;
  font-size: 12px; font-weight: 300; color: var(--tertiary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.shot-img { position: relative; overflow: hidden; line-height: 0; }
.shot-img img { display: block; width: 100%; height: auto; }

/* mask reveal — JS-gated, so with JS off the image is simply visible */
.js-enabled .shot-img img { transform: scale(1.06); clip-path: inset(100% 0 0 0); }
.js-enabled .shot.is-in .shot-img img {
  transform: none; clip-path: inset(0 0 0 0);
  transition: transform 1.15s var(--e-out), clip-path 1s var(--e-out);
}
.js-fallback .shot-img img { transform: none !important; clip-path: none !important; }

.shot:hover { border-color: #aeb6c1; box-shadow: 0 18px 40px -24px rgba(20, 30, 70, .35); }
.js-enabled .shot.is-in:hover .shot-img img { transform: scale(1.035); transition: transform .6s var(--e-out); }

/* placement */
.case-lead .shot, .case-mini .shot, .cs-card .shot { margin-bottom: 26px; }
.cs-card .shot { margin-bottom: 4px; }
.page-shot { margin: 0 0 56px; }
.page-shot .shot-img img { max-height: 620px; object-fit: cover; object-position: top center; }

.work-strip { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--grid-gap); margin: 22px 0 12px; }
.work-strip a { display: grid; gap: 10px; }
.work-strip span { font-size: 15px; font-weight: 400; color: var(--muted); transition: color .25s linear; }
.work-strip a:hover span { color: var(--accent); }

/* =============================================================================
   RESPONSIVE
   ========================================================================== */
@media (max-width: 1180px) {
  :root { --pad: 56px; }
  .recog-grid { grid-template-columns: repeat(2, 1fr); }
  .fact-grid { grid-template-columns: repeat(2, 1fr); }
  .case-cols { grid-template-columns: repeat(2, 1fr); }
  .review-grid { grid-template-columns: 1fr; }
  .foot-grid { grid-template-columns: 1fr 1fr; }
  .sys-inner { gap: 44px; }
  .price-card { grid-template-columns: 1fr; gap: 30px; padding: 38px 34px; }
  .pc-side { justify-content: flex-start; }
  .pc-price { text-align: left; }
  .pc-note { text-align: left; }
  .case-row { grid-template-columns: repeat(2, 1fr); }
}

/* Below 1024px every pinned/sticky sequence becomes ordinary vertical content. */
@media (max-width: 1023px) {
  .sys-scroll { height: auto; }
  .sys-sticky { position: static; height: auto; display: block; }
  .sys-inner { grid-template-columns: 1fr; gap: 0; }
  /* Hide the RAIL, not the whole right column — .sys-done ("One connected
     customer journey") lives inside .sys-right, so `.sys-right { display: none }`
     deleted the section's closing line from every phone AND from anyone with
     reduced motion on. With one column it simply becomes the last row. */
  .sys-rail { display: none; }
  .sys-done { margin-top: 8px; padding-top: 24px; }
  .sys-count { display: none; }
  .sys-panes { position: relative; min-height: 0; padding-left: 30px; }
  /* fade both ends rather than letting the line dangle past the first/last dot */
  .sys-panes::before {
    content: ''; position: absolute; left: 5px; top: 0; bottom: 0; width: 2px;
    background: linear-gradient(180deg, transparent 0,
                var(--hairline) 34px, var(--hairline) calc(100% - 34px), transparent 100%);
  }
  .js-enabled .sys-pane,
  .sys-pane { position: relative; inset: auto; opacity: 1; transform: none; pointer-events: auto; padding: 26px 0; }
  /* the mobile stand-in for an active .sn-dot — same object, same live look */
  .sys-pane::before {
    content: ''; position: absolute; left: -30px; top: 34px;
    width: var(--dot-md); height: var(--dot-md); border-radius: var(--r-round);
    background-image: var(--grad);
    box-shadow: inset 0 0 0 var(--dot-ring) var(--accent), 0 0 0 3px rgba(61, 68, 78, .13);
  }
  .sys-pane h3 { font-size: 34px; }
  .sys-pane > p { font-size: 18px; margin-bottom: 18px; }
  .js-enabled .sys-done { opacity: 1; transform: none; }

  .faq-grid { grid-template-columns: 1fr; gap: 36px; }
  .proc-head, .faq-head { position: static; }

  .price-slot { position: static; }
  .price-slot + .price-slot { margin-top: var(--grid-gap); }
  .price-tail { display: none; }
  .price-card { transform: none !important; max-width: none; }
}

@media (max-width: 900px) {
  :root { --pad: 24px; --sec-gap: 56px; --lift: 32px; }
  .nav-links { display: none; }
  .burger { display: flex; }
  .hero { padding-top: 132px; }
  .team-grid, .cta-grid, .case-row { grid-template-columns: 1fr; }
  .team-panel { order: -1; }
  .gem { display: none; }   /* large blurred conics are not worth the paint cost on a phone */
  .case-cta { flex-direction: column; align-items: flex-start; }
  .page-head { padding-top: 132px; }
  .cs-grid { grid-template-columns: 1fr; }
  .sticky-cta { display: block; }
  /* the bar covers the bottom of the page — keep the footer clear of it */
  .footer { padding-bottom: 108px; }
}

@media (max-width: 620px) {
  :root { --pad: 18px; }
  body { font-size: 17px; }
  h2 { font-size: 30px; }
  .recog-grid, .case-cols, .foot-grid, .row2 { grid-template-columns: 1fr; }
  .hero-title { font-size: clamp(32px, 9vw, 46px); }
  .case-lead, .case-mini, .price-card, .team-panel, .qform, .review, .recog-item, .case-cta {
    padding: 26px 22px; border-radius: 28px;
  }
  .faq-q { font-size: 19px; gap: 14px; }
  .fact-grid, .work-strip { grid-template-columns: 1fr; }
  .fact, .quote-block, .prose-note, .cs-card { padding: 24px 22px; border-radius: 24px; }
  .prose p, .prose-list li { font-size: 17px; }
  .page-next { flex-direction: column; }
  .cmp-head { display: none; }
  .cmp-row { grid-template-columns: 1fr; gap: 0; padding-bottom: 14px; }
  .cmp-old, .cmp-new { padding: 14px; font-size: 16px; }
  .cmp-old { font-size: 17px; }
  /* At this width the bar has one job: one tap. It STACKS — the sub-line is a
     promise, not decoration, and the appended conversion block re-lays it out
     rather than deleting it (see "v19 CONVERSION — AGENT E" at EOF). */
  .sticky-cta { padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px)); }
  .sticky-cta .btn-sm { width: 100%; justify-content: center; font-size: 16px; padding: 14px 18px; }
  .faq-a-inner { padding-right: 10px; }
  .hero-cta .btn, .qform .btn, .drawer .drawer-cta { width: 100%; justify-content: center; }
  .hero-cta { width: 100%; }
  .brand { white-space: nowrap; }
  .nav-right .btn-sm { display: none; }   /* the CTA lives in the drawer at this width */
  .nav-shell { padding-left: 16px; }
  .drawer { padding: 0 18px; }
  .tp-row { padding: 18px 8px; }
  .tp-k { min-width: 0; }
}

/* Touch devices: hover-only affordances must be permanently visible. */
@media (hover: none) {
  .case-lead::after, .case-mini::after,
  .price-card::after, .review::after, .team-panel::after { display: none; }
  .tp-rows:hover .tp-row { opacity: 1; }
  .tp-arw { opacity: .55; transform: rotate(45deg); }
}

/* =============================================================================
   REDUCED MOTION — everything stays visible, nothing animates
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
  .rot-badge, .hero-bg, .cta-panel-bg { transform: none !important; }

  .js-enabled [data-reveal]:not([data-revealed]),
  .js-enabled [data-reveal-lines]:not([data-revealed]),
  .js-enabled [data-hero-step]:not([data-revealed]),
  .js-enabled .sys-done,
  .btn:active { opacity: 1 !important; transform: none !important; }

  /* sticky sequences collapse to plain vertical content */
  .sys-scroll { height: auto !important; }
  .sys-sticky { position: static !important; height: auto !important; display: block !important; }
  .sys-inner { grid-template-columns: 1fr !important; }
  .sys-rail { display: none !important; }
  .js-enabled .sys-tags li { transform: none !important; }
  .sys-count { display: none !important; }
  .js-enabled .sys-pane, .sys-pane {
    position: relative !important; inset: auto !important;
    opacity: 1 !important; transform: none !important; pointer-events: auto !important;
    padding: 22px 0 !important;
  }
  .price-slot { position: static !important; }
  .price-slot + .price-slot { margin-top: var(--grid-gap) !important; }
  .price-tail { display: none !important; }
  .price-card { transform: none !important; }
  .proc-head, .faq-head { position: static !important; }
  .faq-a { height: auto !important; }
  .sys-line-fill, .tp-line-fill { height: 100% !important; }
  .cursor-dot, .cursor-ring { display: none !important; }
  body { cursor: auto !important; }
  .gem { display: none !important; }
  /* the press is a transition, not an animation, but it still needs pinning */
  /* The press is a state change, not an animation, but it still needs pinning.
     Hover keeps its brightening (static, instant, visible — the same call this
     file already makes for nav underlines) and loses its lift. */
  .btn-primary::before, .btn-primary::after { transition: none !important; }
  .btn-primary:hover, .btn-primary:focus-visible { transform: none !important; }
  .btn-primary:hover::after, .btn-primary:focus-visible::after { opacity: 1; }
  .btn-primary:active { transform: none !important; }
  .js-enabled .shot-img img { transform: none !important; clip-path: none !important; }
  .shot:hover { box-shadow: none !important; }
  .case-lead::after, .case-mini::after,
  .price-card::after, .review::after, .team-panel::after { opacity: 0 !important; }

  /* hover lifts are removed, not left mid-transform */
  .case-mini:hover, .case-lead:hover, .review:hover, .recog-item:hover, .price-card:hover {
    transform: none !important; box-shadow: none !important;
  }
  /* every transform introduced by the control-system pass, pinned. The washes,
     borders and colour changes all survive — static and instant, the same call
     this file already makes for the primary's sheen and the nav underlines. */
  .btn-ghost:hover, .btn-ghost:focus-visible { transform: none !important; box-shadow: none !important; }
  .btn-ghost:active, .choice:active, .cur-btn:active, .burger:active,
  .nav-links a:active, .foot-col a:active, .foot-social a:active { transform: none !important; }
  .trust-strip li:hover > *:not(.ts-dot) { transform: none !important; }
  .arw::before, .arw::after { transition: none !important; }
  .btn:hover .arw::before, .btn:focus-visible .arw::before { transform: none !important; opacity: 1 !important; }
  .btn:hover .arw::after, .btn:focus-visible .arw::after { opacity: 0 !important; }
  .nav-links a::after, .foot-col a::after, .drawer a:not(.btn)::after { transform: scaleX(0) !important; }
  .nav-links a:hover::after, .nav-links a.is-active::after,
  .foot-col a:hover::after, .drawer a:not(.btn):hover::after { transform: scaleX(1) !important; }
}

/* =============================================================================
   INTERIOR-PAGE COMPONENTS (v17e migration — services, locations, contact)

   Everything here exists because a live ranking page needs it and no existing
   v17e component covered it. Nothing here is used by the home page.

   Contrast measured on --bg #fbfbfc:
     --muted   #4e535b   7.50:1  text
     --edge    #848a94   3.36:1  boundary  (WCAG 1.4.11 minimum is 3:1)
     --edge-on #6d747e   4.56:1  boundary
     --accent  #3d444e   9.51:1  text
   No opacity below 1 on text. Transform + opacity only. Reduced motion at the
   bottom of the block collapses every hover transform.
   ========================================================================== */

/* -- Inline links inside long-form prose. The base rule is
      `a { color: inherit; text-decoration: none }`, so a prose link is
      completely invisible without this. The underline is permanent — never a
      hover-only tell. */
.prose a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-thickness .2s linear, color .2s linear;
}
.prose a:hover, .prose a:focus-visible { color: var(--accent-dk); text-decoration-thickness: 2px; }

/* -- h3 inside prose. Long-form pages need a third level; nothing on the home
      page did. Sentence case, unlike the global uppercase h2, so the hierarchy
      stays legible rather than shouting twice. */
.prose h3 {
  font-size: clamp(20px, 1.6vw, 23px); font-weight: 600; line-height: 1.25;
  text-transform: none; margin: 34px 0 12px;
}

/* -- Numbered variant of .prose-list. Replaces the tick glyph with a numeral. */
ol.prose-list { counter-reset: pl; }
ol.prose-list li::before {
  content: counter(pl); counter-increment: pl;
  border: 0; transform: none; width: auto; height: auto;
  left: 0; top: 0; font-size: 15px; font-weight: 600; color: var(--accent);
}

/* -- .cs-card carrying an h3 instead of an h2: service cards on a location page
      are subordinate to that page's section h2. Same sizing intent as
      `.cs-card h2`, one step down. */
.cs-card h3 {
  font-size: 24px; font-weight: 600; text-transform: none; line-height: 1.18;
  transition: transform .4s var(--e-out);
}
.cs-card:hover h3 { transform: translateX(5px); }

/* -- Chip links: adjacent suburbs, related services, cross-links. These are
      controls, so the boundary is --edge (3.36:1), not --hairline (1.37:1). */
.link-chips { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 6px; }
.link-chips a {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 16px; font-weight: 300; color: var(--muted);
  border: 1px solid var(--edge); border-radius: var(--r-round);
  padding: 9px 18px;
  transition: color .25s linear, border-color .25s linear, transform .3s var(--e-out);
}
.link-chips a:hover, .link-chips a:focus-visible {
  color: var(--accent); border-color: var(--edge-on); transform: translateY(-2px);
}

/* -- A link inside a .fact value (contact details). Without this the base
      `a { color: inherit; text-decoration: none }` makes a mailto: or tel:
      indistinguishable from the text beside it. */
.fact-v a {
  color: var(--accent); text-decoration: underline;
  text-decoration-thickness: 1px; text-underline-offset: 3px;
  transition: text-decoration-thickness .2s linear, color .2s linear;
}
.fact-v a:hover, .fact-v a:focus-visible { color: var(--accent-dk); text-decoration-thickness: 2px; }

/* -- Meta line under a page-head: postcode / region / pricing. --tertiary is
      5.9:1, above the 4.5:1 floor; the <strong> lifts to --muted at 7.5:1. */
.head-meta { margin-top: 18px; font-size: 16px; font-weight: 300; color: var(--tertiary); }
.head-meta strong { color: var(--muted); font-weight: 400; }

/* -- Spacing for the second and later blocks inside one open .section, on
      pages that stack several headed groups in a single container. A class,
      not a repeated inline style, so the rhythm is defined once. */
.sec-head-next { margin-top: 64px; }

@media (max-width: 620px) {
  .link-chips a { font-size: 15px; padding: 8px 15px; }
  .sec-head-next { margin-top: 48px; }
}

@media (prefers-reduced-motion: reduce) {
  .link-chips a:hover, .link-chips a:focus-visible { transform: none !important; }
  .cs-card:hover h3 { transform: none !important; }
  .prose a { transition: none !important; }
}

/* ---------------------------------------------------------------------------
   SHADOW TEST — the inverted half. On the charcoal CTA band and the footer a
   black shadow is literally invisible, so every raised object there has been
   sitting flat. Same geometry, white light. Scoped to dark surfaces only, so
   nothing on the light page is touched.
   REVERT: delete this block.
   ------------------------------------------------------------------------ */
.cta-band .btn-primary,
.footer .btn-primary {
  box-shadow: 0 0.06em 0.10em -0.045em rgba(255, 255, 255, .34),
              0 0.40em 0.90em -0.55em  rgba(255, 255, 255, .40);
}
.cta-band .btn-primary:hover,
.cta-band .btn-primary:focus-visible,
.footer .btn-primary:hover,
.footer .btn-primary:focus-visible {
  box-shadow: 0 0.06em 0.10em -0.045em rgba(255, 255, 255, .40),
              0 0.72em 1.45em -0.60em  rgba(255, 255, 255, .52);
}
.cta-band .qform,
.cta-band .form-done {
  box-shadow: 0 0.40em 1.10em -0.50em rgba(255, 255, 255, .22),
              0 24px 60px -40px rgba(0, 0, 0, .55);
}
.footer .foot-social a:hover {
  box-shadow: 0 0.30em 0.70em -0.45em rgba(255, 255, 255, .38);
}
@media (prefers-reduced-motion: reduce) {
  .cta-band .btn-primary:hover, .footer .btn-primary:hover { box-shadow: none !important; }
}


/* ===== v19 INK BAND — AGENT C ===== */

/* -----------------------------------------------------------------------------
   THE SURFACE. One scoped token re-point; no parallel rule set.

   --band is the only token the existing
     `#system, #process, #team, #faq { background: var(--band) }`
   rule reads, so re-pointing it here paints the band ink WITHOUT editing that
   rule — and .sn-dot masks the rail with `--dot-fill: var(--band)`, so the dot's
   opaque mask follows the surface for free.

   `color: #fff` is NOT redundant with --text. `body { color: var(--text) }`
   resolves --text ON BODY, so h2/h3 inherit a COMPUTED #24272d (1.21:1 on ink) —
   re-pointing --text here would never reach them. Biggest trap in the conversion.
   -------------------------------------------------------------------------- */
#system {
  --band: var(--ink);
  color: #fff;

  --text:       #ffffff;            /* 18.05:1  h3, promise line, #sysNum, active label */
  --muted:      #a2a9b3;            /*  7.62:1  lead, tags, completed rail label        */
  --tertiary:   #868d98;            /*  5.39:1  "/ 05", upcoming rail label  (AA)       */

  /* THE INVERSION. On white the metal ramp is the palest thing on the page, which
     is why this component had to abandon it. On ink the ramp is the LIT state. */
  --accent:     #ffffff;            /* 18.05:1  active, hover, .sys-done, fill tip      */
  --accent-mid: var(--metal-lite);  /* #d8dee6 — 13.33:1  eyebrow, travelled light      */
  --accent-dk:  var(--metal-lite);

  /* --hairline is 1.37:1 on the page but 12.76:1 on ink — an UNLIT groove that
     would out-shout the lit fill. Mandatory flip. */
  --hairline:   #343941;            /*  1.55:1  rail groove, .sys-done divider          */
  --edge:       #656c77;            /*  3.41:1  tag-chip boundary                       */
  --edge-on:    #7d8490;            /*  4.79:1                                          */
  --card:       var(--ink);         /* chips are outlined, not filled                   */

  /* Thread hooks (Agent B) — flip with the surface for free. */
  --thread-line: var(--hairline);
  --thread-lit:  var(--accent-mid);
  --thread-node: var(--accent);
}

/* THE RAIL ON INK — upcoming 3.03:1 ring / completed 4.98:1 disc / active metal
   face 12.34–17.60:1 (2.48:1 brighter than a completed disc). */
#system .sn-dot { --dot-line: #5d646f; }
#system .sys-node.is-done .sn-dot { --dot-fill: #7f8794; --dot-line: #7f8794; }
#system .sys-node.is-active .sn-dot { box-shadow: 0 0 0 6px rgba(255, 255, 255, .13); }
#system .sys-node.is-active .sn-dot::after { opacity: 1; box-shadow: none; }

/* THE PROGRESS LINE. Was --accent-mid -> --accent: on ink that is 2.94 -> 1.84:1,
   i.e. the completed line would be DARKER than the groove it replaces — the light
   build's old bug in mirror image. Reversed, brightening toward the leading edge. */
#system .sys-line-fill {
  background: linear-gradient(180deg, #7f8794 0%, var(--metal-lite) 55%, var(--gem-lit) 100%);
}

@media (max-width: 1023px) {
  #system .sys-pane::before { box-shadow: 0 0 0 3px rgba(255, 255, 255, .14); }
}

/* THE SEAMS. Existing .sec-lift geometry, unchanged — only the value changes, so
   the reader reads a colour decision, not a new shape. 120px of silent ink opens
   and closes the band instead of arriving at a 17.43:1 edge 26px from content. */
.sys-sec { padding-bottom: calc(var(--sec-gap) + var(--lift)); }

/* THE NO-TRACK SHAPE. At >=1024 with JS blocked the 260vh track and the 84vh
   centred sticky box BOTH applied while .sys-pane was un-stacked: ~1125px of
   content in an 806px box, overflowing ~160px UPWARD out of the section, pinned
   for 260vh. On ink that paints white text over the light section above.
   Fix: collapsed shape is the BASE; the track is a .js-enabled enhancement gated
   on exactly the condition isDesktopMotion() uses, so CSS and JS cannot disagree. */
.sys-scroll { position: relative; }
.sys-sticky { display: block; }
.sys-inner  { display: grid; grid-template-columns: 1fr; gap: 0; width: 100%; }
.sys-rail, .sys-count { display: none; }

@media (min-width: 1024px) {
  .js-enabled .sys-scroll { height: 260vh; }
  .js-enabled .sys-sticky { position: sticky; top: 11vh; height: 84vh; display: flex; align-items: center; }
  .js-enabled .sys-inner  { grid-template-columns: minmax(0, 1fr) minmax(180px, 208px); gap: 80px; align-items: center; }
  .js-enabled .sys-rail   { display: block; }
  .js-enabled .sys-count  { display: block; }
  .sys-pane + .sys-pane { border-top: 1px solid var(--hairline); margin-top: 26px; padding-top: 26px; }
}
@media (min-width: 1024px) and (max-width: 1180px) { .js-enabled .sys-inner { gap: 44px; } }

/* only the LIVE sticky version drops the dividers — reduced motion stacks too */
@media (min-width: 1024px) and (prefers-reduced-motion: no-preference) {
  .js-enabled .sys-pane + .sys-pane { border-top: 0; margin-top: 0; padding-top: 0; }
}

/* watchdog: the scrub never ran — collapse the track, kill the dead indicators */
.js-fallback .sys-scroll { height: auto !important; }
.js-fallback .sys-sticky { position: static !important; height: auto !important; display: block !important; }
.js-fallback .sys-inner  { grid-template-columns: 1fr !important; gap: 0 !important; }
.js-fallback .sys-rail, .js-fallback .sys-count { display: none !important; }
@media (min-width: 1024px) {
  .js-fallback .sys-pane + .sys-pane {
    border-top: 1px solid var(--hairline) !important; margin-top: 26px !important; padding-top: 26px !important;
  }
}

/* SHORT-VIEWPORT GUARD. The thesis h2 is now the largest heading in the page body,
   so the pinned frame is taller: ~584px inside a 645px box at 1024x768. Below
   ~700px of viewport height it would overflow the pin. */
@media (min-width: 1024px) and (max-height: 760px) {
  .sys-head h2 { font-size: clamp(30px, 2.6vw, 38px); }
  .sys-head    { margin-bottom: 28px; padding-bottom: 22px; }
}
/* ===== end ===== */

/* ===== v19 THREAD + TYPE — AGENT B ===== */
/* Everything here is scoped to main[data-thread] = the HOME PAGE ONLY. No
   interior page, no shared partial and no existing declaration changes. */

/* ---------- B1. THE THREAD — tokens ---------- */
[data-thread] {
  counter-reset: chapter;
  --thr-inset: 44px;
  /* `100%` resolves against the pseudo's containing block = the section's padding
     box = the viewport width, so this tracks --container and --pad with no JS. */
  --thr-x: calc(max(0px, (100% - var(--container)) / 2) + var(--pad) - var(--thr-inset));
  --thr-line: var(--silver);
  --thr-lit:  linear-gradient(180deg, var(--accent-mid), var(--accent));
  --thr-cap:  var(--accent);
}

/* FAIL-OPEN, AS THE DEFAULT, NOT A PATCH. .is-scrub is added by initThread() only
   after it has built the light layer. Until then — JS off, module blocked, a throw,
   reduced motion — the TRACK is painted in the LIT colour, so the thread renders
   fully lit and static with no !important anywhere and no gate to forget. */
[data-thread]:not(.is-scrub)              { --thr-line: var(--accent-mid); }
[data-thread]:not(.is-scrub) > .cta-band  { --thr-line: var(--metal-lite); }

/* Dark surfaces re-point the token — the same move this file makes for --focus. */
[data-thread] > .cta-band {
  --thr-line: rgba(216, 222, 230, .30);
  --thr-lit:  linear-gradient(180deg, var(--metal-lite), var(--gem-lit));
  --thr-cap:  var(--gem-lit);
  --thr-node-ring: var(--metal-lite);
  --thr-node-halo: rgba(216, 222, 230, .20);
  /* Neither is a magic number: 80px IS .cta-band's margin-top (the only real gap
     in the page), 100px IS its padding-bottom (thread stops level with the form). */
  --thr-lead: 80px;
  --thr-tail: 100px;
}

/* THE INK BAND — same contract (Agent B section 7.4 / Agent C). */
[data-thread] > #system {
  --thr-line: rgba(216, 222, 230, .30);
  --thr-lit:  linear-gradient(180deg, var(--metal-lite), var(--gem-lit));
  --thr-cap:  var(--gem-lit);
  --thr-node-ring: var(--metal-lite);
  --thr-node-halo: rgba(216, 222, 230, .20);
}
[data-thread]:not(.is-scrub) > #system { --thr-line: var(--metal-lite); }

/* The hero segment is NOT Agent B's. .hero has overflow:hidden, and the hero
   needs a struck light with a node on it — so Agent A owns that segment as real
   elements inside .container, on the SAME centre line (--thread-x below resolves
   to the identical x). Agent B's thread starts at #how. */

/* ---------- B2. THE THREAD — the segments ----------
   Zero layout cost: both boxes are position:absolute, out of flow, generate no
   line boxes, contribute nothing to their containing block's height. No section
   gains a positioning context — .section, .hero and .cta-band are ALREADY
   position:relative. No `left` is ever animated. */
[data-thread] > .section::before,
[data-thread] > .cta-band::before,
[data-thread] .thr-lit {
  content: '';
  position: absolute;
  z-index: 2;                       /* above .sec-lift (1), far below .nav (100) */
  /* -5.5px = half the 11px box. The LINE is painted at 50% of the box, so
     without this it lands at --thr-x + 5.5 while the hero centres its own line
     ON --thr-x — a measured 5.5px kink at the hero seam, on the one element the
     whole page is built around. */
  left: calc(var(--pad) - var(--thr-inset) - 5.5px);         /* fallback, no max() */
  left: calc(var(--thr-x) - 5.5px);
  width: 11px;                      /* the box; the LINE inside it is --thread-w */
  top: calc(var(--thr-lead, 0px) * -1);
  bottom: var(--thr-tail, 0px);
  pointer-events: none;
  background-size: var(--thread-w) 100%;
  background-position: 50% 0;
  background-repeat: no-repeat;
}

[data-thread] > .section::before,
[data-thread] > .cta-band::before {
  background-image: linear-gradient(var(--thr-line), var(--thr-line));
}

/* scaleY(1) is the RESTING value, so a throw at any point after the element
   exists leaves the thread fully lit rather than dark. */
[data-thread] .thr-lit {
  background-image: var(--thr-lit);
  transform-origin: 50% 0;
  transform: scaleY(1);
  will-change: transform;
}

/* THE TERMINUS — a second background layer on the track, so it costs no
   element and cannot be squashed by the light's scaleY. (The hero draws its own
   start cap; its segment is Agent A's, not this one.) */
[data-thread] > .cta-band::before {
  background-image: linear-gradient(var(--thr-line), var(--thr-line));
}

/* scaleY(1) is the RESTING value, so a throw at any point after the element
   exists leaves the thread fully lit rather than dark. */
[data-thread] .thr-lit {
  background-image: var(--thr-lit);
  transform-origin: 50% 0;
  transform: scaleY(1);
  will-change: transform;
}

/* THE TWO ENDPOINTS — a second background layer on the track, so they cost no
   element and cannot be squashed by the light's scaleY. */
[data-thread] > .cta-band::before {
  background-image: linear-gradient(var(--thr-line), var(--thr-line));
}

/* scaleY(1) is the RESTING value, so a throw at any point after the element
   exists leaves the thread fully lit rather than dark. */
[data-thread] .thr-lit {
  background-image: var(--thr-lit);
  transform-origin: 50% 0;
  transform: scaleY(1);
  will-change: transform;
}

/* THE TWO ENDPOINTS — a second background layer on the track, so they cost no
   element and cannot be squashed by the light's scaleY. */
[data-thread] > .hero::before {
  background-image:
    radial-gradient(circle 4.5px at 50% 50%, var(--thr-cap) 99%, transparent 100%),
    linear-gradient(var(--thr-line), var(--thr-line));
  background-size: 11px 9px, 1px 100%;
  background-position: 50% 0, 50% 0;
}
[data-thread] > .cta-band::before {
  background-image:
    radial-gradient(circle 5px at 50% 50%, var(--thr-cap) 99%, transparent 100%),
    linear-gradient(var(--thr-line), var(--thr-line));
  background-size: 11px 10px, var(--thread-w) 100%;
  background-position: 50% 100%, 50% 0;   /* the terminus sits AT the form's foot */
}

/* ---------- B3. CHAPTER MARKERS ---------- */
[data-thread] .eyebrow[data-chapter],
[data-thread] > .cta-band .eyebrow {
  position: relative;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: .12em;
  text-transform: uppercase;
  margin-bottom: 20px;
  max-width: none;
}
[data-thread] .eyebrow[data-chapter]    { color: var(--accent-mid); }
[data-thread] > .cta-band .eyebrow      { color: var(--metal-lite); }

[data-thread] .eyebrow[data-chapter] { counter-increment: chapter; }
[data-thread] .ch-n::before {
  content: counter(chapter, decimal-leading-zero) " \2014\00a0";
  color: var(--text);
  font-weight: 600;
}

/* THE NODE ON THE THREAD. The eyebrow's left edge IS the container's content edge
   in all eight sections — including the three in a grid and the one in a sticky
   column, because all four are the FIRST grid track and start on the same rail. */
[data-thread] .eyebrow[data-chapter]::before,
[data-thread] > .cta-band .eyebrow::before {
  content: '';
  position: absolute;
  left: calc(var(--thr-inset) * -1 - 4.5px);
  top: calc(.7em - 4.5px);
  width: 9px; height: 9px;
  border-radius: var(--r-round);
  background-image: var(--grad);
  box-shadow: inset 0 0 0 var(--dot-ring-thin) var(--thr-node-ring, var(--accent)),
              0 0 0 3px var(--thr-node-halo, rgba(61, 68, 78, .13));
  pointer-events: none;
}
/* the CTA node is the TERMINUS, so it reads one step heavier than a chapter */
[data-thread] > .cta-band .eyebrow::before {
  width: 11px; height: 11px;
  left: calc(var(--thr-inset) * -1 - 5.5px);
  top: calc(.7em - 5.5px);
}

/* ---------- B4. THE TYPE SCALE ----------
   h1 > h2 > h3 > lead at EVERY width, structurally: floor 40>30, slope 6.6vw>4.0vw,
   cap 84>58, and clamp() is monotone in each argument. The documented 621-1167px
   inversion needed a FLAT 42px in a fluid ladder; no flat value is left. */
[data-thread] .hero-title {
  font-size: clamp(40px, 6.6vw, 84px);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: -.032em;
  max-width: 19ch;
  margin: 0 0 28px;
}

/* SENTENCE CASE. Uppercase fills ~100% of its em box vs ~72% for mixed, so a 58px
   uppercase h2 has the presence of ~80px against an 84px h1 — ratio 1.05, i.e. the
   flat hierarchy again. These strings are sentences with verbs and full stops.
   Uppercase now means: small type, or one word. Never a sentence. */
[data-thread] h2 {
  font-size: clamp(30px, 4.0vw, 58px);
  font-weight: 600;
  line-height: 1.06;
  letter-spacing: -.022em;
  text-transform: none;
}
[data-thread] .sec-head h2 { max-width: 19ch; }

/* #system keeps a smaller h2 (it rides a sticky column beside the stage word) but
   is no longer SMALLER THAN ITS OWN h3, which it was: 34 vs 52. */
[data-thread] .sys-head h2 { font-size: clamp(30px, 3.1vw, 46px); line-height: 1.08; max-width: 18ch; }

/* THE DISPLAY h3 TIER. Uppercase 800 stays — single words, names, one shout per
   block. Capped so no h3 can out-shout the h2 above it. */
[data-thread] .sys-pane h3 { font-size: clamp(26px, 2.4vw, 34px); }
[data-thread] .pc-top h3   { font-size: clamp(26px, 2.6vw, 38px); }

[data-thread] .lead {
  font-size: clamp(18px, 1.05vw + 6.9px, 22px);
  line-height: 1.55;
}
[data-thread] .sec-head  { max-width: 1040px; margin-bottom: 52px; }
[data-thread] .sec-lead  { margin-top: 20px; max-width: 62ch; }
[data-thread] .sys-lead  { font-size: clamp(17px, .8vw + 8.5px, 19px); max-width: 46ch; }

/* ---------- B5. SECTION RHYTHM — three acts, expressed as air ----------
   The page paid exactly 72px between every one of ten sections, which is why a
   fast scroll read as one block. --sec-gap feeds .section's padding and
   .sec-lift's padding-top; --lift feeds .sec-lift's margin-top and padding-top,
   so one value moves BOTH halves of a seam and they cannot disagree. */
[data-thread]                { --sec-gap: 88px; }
[data-thread] > #system      { --sec-gap: 120px; --lift: 64px; }
[data-thread] > #results     { --sec-gap: 112px; }
[data-thread] > #reviews,
[data-thread] > #faq         { --sec-gap: 76px; }

/* ---------- B6. RESPONSIVE ---------- */
@media (max-width: 1180px) {
  [data-thread] { --thr-inset: 30px; }     /* --pad drops to 56; clearance stays >= 25px */
}

/* BELOW 1024 THE THREAD DOES NOT EXIST. --pad is 24px (18 below 620) — no gutter
   to run a line in without touching copy or hugging the viewport edge like a
   rendering artefact. It is also where the build already drops every pinned
   sequence and where isDesktopMotion() stops firing, so ONE number governs "this
   page has scroll choreography". The MARKERS survive at every width. */
@media (max-width: 1023px) {
  [data-thread] > .section::before,
  [data-thread] > .cta-band::before,
  [data-thread] .thr-lit,
  [data-thread] .eyebrow[data-chapter]::before,
  [data-thread] > .cta-band .eyebrow::before { display: none; }

  [data-thread] .sys-pane h3 { font-size: 26px; }   /* 34 beat the 30px h2 floor */
  [data-thread]              { --sec-gap: 72px; }
  [data-thread] > #system    { --sec-gap: 88px;  --lift: 40px; }
  [data-thread] > #results   { --sec-gap: 84px; }
  [data-thread] > #reviews,
  [data-thread] > #faq       { --sec-gap: 64px; }
}

@media (max-width: 900px) {
  [data-thread]              { --sec-gap: 60px; }
  [data-thread] > #system    { --sec-gap: 76px;  --lift: 32px; }
  [data-thread] > #results   { --sec-gap: 72px; }
  [data-thread] > #reviews,
  [data-thread] > #faq       { --sec-gap: 56px; }
}

/* ---------- B7. THE THREE FAIL-OPEN GATES ---------- */
.js-fallback [data-thread]             { --thr-line: var(--accent-mid); }
.js-fallback [data-thread] > .cta-band { --thr-line: var(--metal-lite); }
.js-fallback [data-thread] > #system   { --thr-line: var(--metal-lite); }
.js-fallback [data-thread] .thr-lit    { transform: scaleY(1) !important; }

@media (prefers-reduced-motion: reduce) {
  [data-thread]             { --thr-line: var(--accent-mid); }
  [data-thread] > .cta-band { --thr-line: var(--metal-lite); }
  [data-thread] > #system   { --thr-line: var(--metal-lite); }
  [data-thread] .thr-lit    { transform: scaleY(1) !important; }
}
/* ===== end ===== */

/* ===== v19 HERO — AGENT A ===== */

/* ---------------------------------------------------------------------------
   THE HERO OWNS THE THREAD'S FIRST SEGMENT.

   Agent B's page thread is a ::before per section. .hero cannot use it: the
   section carries `overflow: hidden` (it clips .gem and .hero-bg), and the hero
   needs three layers, not one — a track, a lit fill, and a travelling light that
   strikes the line into existence. So the hero draws its own segment as real
   elements inside .container, and B's thread starts at #how.

   The two are COLLINEAR by construction, not by coincidence:
       B:  section left + max(0,(100%-container)/2) + --pad - --thr-inset
       A:  .container left border            + --thread-x
   and .container's left border edge IS `max(0,(100vw-container)/2)`, so setting
   --thread-x to (--pad - --thr-inset) puts both lines on the same pixel at every
   width. Change --thr-inset and both move together.
   ------------------------------------------------------------------------ */
[data-thread] {
  --thread-w:    2px;
  --thread-x:    calc(var(--pad) - var(--thr-inset));
  --hero-indent: 0px;
  /* the same centre line, expressed relative to .hero-copy's CONTENT box, which
     is where .hero-node and the trust strip's rule reach it from. Negative. */
  --ht-offset:   calc(var(--thread-x) - var(--pad) - var(--hero-indent));
}

/* ---------- the copy column ---------- */
.hero-copy { position: relative; padding-left: var(--hero-indent); }

/* THE META ROW. The old rule was `flex-wrap: wrap` with NO media query, so it
   silently became two rows near 1274px — and because --pad steps 96 -> 56 at
   1180px the wrap was not even monotonic: it wrapped 1181-1221, UNwrapped
   1142-1180, and wrapped again below 1142. Three transitions in a 100px band.
   Above 900px the structure is now fixed — nowrap, and the eyebrow re-wraps
   inside the row. Text taking a second line is reflow; a component changing its
   box model is a flip, and there is now exactly one, at a declared width. */
.hero-meta { flex-wrap: nowrap; gap: 20px; margin-bottom: 32px; }
.hero-meta .status-pill { flex: none; }
.hero .eyebrow { flex: 1 1 auto; min-width: 0; }

/* the lead's rule moves off the deleted .hero-body wrapper. Measure unchanged. */
.hero .lead { max-width: 58ch; margin-bottom: 36px; }

.hero-cta { position: relative; margin-bottom: 44px; }

/* THE PLINTH. The strip's border-top is the only horizontal rule in the hero and
   it now SPRINGS FROM THE SPINE: the negative margin pulls its box left to the
   thread's centre line, the equal padding puts the pills back exactly where they
   were. Net layout change for the content: zero. Hero-scoped — eight other pages
   use .trust-strip inside .page-head and must not move. */
.hero .trust-strip {
  padding-top: 28px;
  margin: 0 0 0 var(--ht-offset);
  padding-left: calc(var(--ht-offset) * -1);
}

/* The one string kept from .flow-card, and the only explanation in the hero. The
   thread runs past it and off the hero's bottom edge — the second beat: the line
   does not stop here. 5.12:1 on --bg. */
.hero-thread-cap {
  margin-top: 28px;
  font-size: 15px; font-weight: 400; letter-spacing: .01em;
  color: var(--tertiary);
}

/* ---------- the thread ----------------------------------------------------
   Absolute against .container (already position: relative), from the container's
   top edge — 132px into the hero, 46px clear of the nav shell — to past the
   hero's bottom border edge, where `overflow: hidden` clips it. Both ends are
   bound to box edges and neither is ever animated: wrap-proof, resize-proof,
   zero CLS by construction.
   ------------------------------------------------------------------------ */
.hero-thread {
  position: absolute; z-index: 0; pointer-events: none;
  left: calc(var(--thread-x) - var(--thread-w) / 2);
  top: 0;
  bottom: calc(var(--sec-gap) * -1);
  width: var(--thread-w);
}
.ht-track, .ht-fill { position: absolute; inset: 0; border-radius: var(--thread-w); }
.ht-track { background: var(--hairline); }               /* 1.37:1 — decorative */
.ht-fill {
  background: var(--accent);                             /* 9.51:1 — the lit line */
  box-shadow: 0 0 6px rgba(61, 68, 78, .22);
  transform-origin: top center;
  transform: scaleY(1);          /* THE DEFAULT IS FULLY LIT — JS off, no motion */
  will-change: transform;
}
/* the unlit state is the ONLY thing behind the JS gate, and the gate releases
   itself the moment the strike completes */
.js-enabled .hero-thread:not([data-struck]) .ht-fill { transform: scaleY(0); }

/* THE STRIKE'S HEAD. A pure animation artefact, so with JS off it does not exist
   at all. font-size is set to its own diameter because --metal-inset is written
   in em — the same trick .mctl-ico uses to scale a bevel to a control-sized disc.
   The white core keeps --gem-lit alive; it is otherwise orphaned by the removal
   of the flow rail. */
/* v19a: .hj-light joins the selector list rather than copying the recipe. There
   is ONE travelling light on this page and this is where it is described; the
   journey's head is the same object, re-pointed at a different origin (see the
   v19a block at EOF), never a second light with a second look. */
.ht-light, .hj-light { display: none; }
.js-enabled .ht-light,
.js-enabled .hj-light {
  display: block; position: absolute;
  top: 0; left: 50%;
  width: var(--dot-md); height: var(--dot-md); font-size: var(--dot-md);
  margin: calc(var(--dot-md) / -2) 0 0 calc(var(--dot-md) / -2);
  border-radius: var(--r-round);
  background-color: var(--metal-back);      /* fallback body only — never seen */
  background-image:
    radial-gradient(circle at 34% 30%, var(--gem-lit) 0 20%, transparent 58%),
    var(--grad);
  /* Halo extent is capped at spread + blur/2 = 19px, so the outermost pixel of
     the glow lands 74px from the container edge while the copy starts at 96px.
     It can never wash under text and can never lower a contrast ratio. */
  box-shadow: var(--metal-inset),
              0 0 0 1.5px var(--accent),
              0 0 12px 3px rgba(170, 178, 189, .8),
              0 0 26px 6px rgba(223, 228, 235, .5);
  opacity: 0;
  will-change: transform, opacity;
}

/* CONSTANT VELOCITY, one pass, then gone. Both halves are CSS animations driven
   by one custom property, so the strike runs on the compositor: no rAF frame, no
   Motion One API surface, transform and opacity only. */
.hero-thread.is-struck .ht-fill  { animation: htDraw 1.4s linear .35s forwards; }
.hero-thread.is-struck .ht-light { animation: htRun   1.4s linear .35s forwards,
                                              htFlash 1.4s linear .35s forwards; }
@keyframes htDraw  { from { transform: scaleY(0) }            to { transform: scaleY(1) } }
@keyframes htRun   { from { transform: translate3d(0, 0, 0) }
                     to   { transform: translate3d(0, var(--ht-travel, 640px), 0) } }
@keyframes htFlash { 0% { opacity: 0 } 6%, 90% { opacity: 1 } 100% { opacity: 0 } }

/* ---------- the node -------------------------------------------------------
   The only other metal in the hero, and the second of the direction's three
   sanctioned metal moments (CTA / travelling light / active marker). It joins the
   MARKER FAMILY by selector-list edit, so it inherits the radius, the ring recipe,
   the transition and the live overlay with no duplicated CSS. top: 28px is half a
   .btn (18 + 18 + 18 + 2px border = 56px), so it centres on the first button at
   every width, including the stacked layout below 620px.
   ------------------------------------------------------------------------ */
.hero-node {
  position: absolute; z-index: 1; pointer-events: none;
  width: var(--dot-md); height: var(--dot-md);
  left: var(--ht-offset); top: 28px;
  transform: translate(-50%, -50%);
  --dot-line: var(--dot-done);     /* the family's DONE ring while it waits */
}
.hero-node::after { opacity: 1; }                              /* LIT = the default */
.js-enabled .hero-node:not([data-lit])::after { opacity: 0; }

/* The copy now occupies the left ~60% at desktop, so the facet field's hole moves
   left with it: the facets bloom in the space .flow-card used to fill and the
   headline still sits on clean paper. Decorative; already display:none below
   900px and under reduced motion. */
.hero .gem {
  -webkit-mask-image: radial-gradient(112% 96% at 33% 44%, transparent 0%, transparent 32%, #000 80%);
          mask-image: radial-gradient(112% 96% at 33% 44%, transparent 0%, transparent 32%, #000 80%);
}

/* ---------- THE HERO'S ONE LAYOUT BREAKPOINT ------------------------------
   900px, because that is where this page ALREADY swaps the nav for a burger,
   --pad from 56 to 24, and four other components — so the whole page changes
   personality at one declared width rather than one component changing alone at
   a width nobody chose. Two things happen and nothing else:
     1. the meta row becomes an explicit stack
     2. the thread leaves the gutter (at --pad 18 a 2px line would sit 9px from
        the screen edge and read as a rendering artefact) and moves to the content
        edge; the copy indents 26px to clear it. At 390px: thread at x=18, node
        spanning 11-25px, copy at x=44.
   ------------------------------------------------------------------------ */
@media (max-width: 900px) {
  [data-thread] { --thread-x: var(--pad); --hero-indent: 26px; }
  .hero-meta { display: block; margin-bottom: 28px; }
  .hero-meta .status-pill { margin-bottom: 12px; }
  .hero .lead { margin-bottom: 32px; }
  .hero-cta   { margin-bottom: 36px; }
  /* the light is only 19px from the copy here, so the halo is trimmed to an 11px
     extent — it still cannot reach text, at any density */
  .js-enabled .ht-light,
  .js-enabled .hj-light {
    box-shadow: var(--metal-inset),
                0 0 0 1.5px var(--accent),
                0 0 10px 2px rgba(170, 178, 189, .75);
  }
}

/* ---------- the three fail-open gates ------------------------------------- */
.js-fallback .ht-fill        { transform: scaleY(1) !important; }
.js-fallback .ht-light,
.js-fallback .hj-light       { display: none !important; }
.js-fallback .hero-node::after { opacity: 1 !important; }

@media (prefers-reduced-motion: reduce) {
  /* the thread renders fully lit and static — still a thread, no scrub */
  .ht-fill  { transform: scaleY(1) !important; }
  .ht-light { display: none !important; }
  .hero-node::after { opacity: 1 !important; }
  .hero-thread.is-struck .ht-fill,
  .hero-thread.is-struck .ht-light { animation: none !important; }
}
/* ===== end ===== */

/* v19: the facet wash is REMOVED from the hero. With the copy in one editorial
   column the right third is clean paper, and a masked conic field there did not
   read as polish — it read as a smudge on an otherwise empty page. The gem
   survives where it has something to sit behind: the CTA band. */
.hero .gem { display: none; }

/* ===== v19 PROOF + OFFER — AGENT D ===================================== */
/* Act III, six sections, six shapes. APPEND AT EOF: this block carries its own
   responsive and reduced-motion tails and they only work if nothing follows.
   Every selector is ID-scoped (#results / #process / #ways / #team / #reviews /
   #faq) for two reasons: .tp-*, .team-panel, .faq-row, .review::after and
   .shot are also shipped by /about/, /case-studies/ and /services/seo/, and ID
   scoping means a later agent block appended after this one cannot silently
   win on equal specificity.
   Nothing here animates width, height, top, left or inset. Nothing here puts
   meaning below opacity 1. No hover or focus state below lowers contrast.
   Nothing here is written on a top-level <section>: no overflow, no isolation,
   no filter, no contain, no transform, no opacity — the Thread's ::before seam
   overlap survives untouched, and section air is still --sec-gap / --lift. */

/* ---------------------------------------------------------------------------
   5. SELECTED CLIENT WORK — THE EXHIBIT WALL                      volume 4
   No clip, no control, no gate. The .js-enabled / .js-fallback pair that used
   to hold 13 paragraphs at max-height 0 has nothing left to hold: the fold is
   gone, not defended.
   ------------------------------------------------------------------------ */
#results .exhibits { display: grid; gap: 96px; }

#results .exhibit {
  display: grid;
  grid-template-columns: minmax(0, 1.06fr) minmax(0, .94fr);
  gap: 0 56px;
  align-items: start;
}
/* Explicit placement, not `order` — order on a grid item does not move it
   between columns, and the zig-zag is the point: the eye is thrown left, right,
   left, right down the section instead of running one straight gutter. */
#results .exhibit:nth-child(even) > .exh-shot { grid-column: 2; grid-row: 1; }
#results .exhibit:nth-child(even) > .exh-copy { grid-column: 1; grid-row: 1; }

/* The proof stays on screen for as long as you are reading about it. sticky is
   not motion, but .proc-head and .faq-head are already un-stuck under reduced
   motion, so this follows the same call (see the tail at the bottom). */
@media (min-width: 1024px) {
  #results .exh-shot { position: sticky; top: 128px; }
}

#results .exh-sector {
  font-size: 16px; font-weight: 300; color: var(--accent-mid);
  margin-bottom: 6px; transition: color .3s linear;
}
#results .exhibit:hover .exh-sector { color: var(--accent-dk); }
/* INTEGRATOR: floor lowered 26 -> 22. The live [data-thread] h2 scale bottoms
   out at 30px, and the house rule is h3 < 0.78 x h2 at EVERY width; 26/30 is
   0.87. 22/30 = 0.73, and the cap is untouched (34/58 = 0.59). */
#results .exh-copy h3 {
  font-size: clamp(22px, 2.4vw, 34px); font-weight: 600;
  line-height: 1.1; letter-spacing: -.015em;
}

/* THE LEDGER. The label becomes a rail rather than a caption: 12px, tracked,
   uppercase, in its own column, so four cases read as four spec sheets instead
   of four blocks of prose. text-transform is presentational — every label
   string is byte-identical in the DOM, in innerText and to a screen reader. */
#results .exh-lines { margin-top: 24px; }
#results .exh-lines .case-line {
  display: grid; grid-template-columns: 150px minmax(0, 1fr);
  gap: 2px 22px; padding: 18px 0;
  border-top: 1px solid var(--hairline);
}
#results .exh-lines .case-line .l {
  margin: 0; padding-top: 4px;
  font-size: 12px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent-mid);
}
#results .exh-lines .case-line .case-body {
  font-size: 17px; font-weight: 300; line-height: 1.55; color: var(--muted);
}
#results .exh-copy .case-link { margin-top: 26px; }
#results .case-cta { margin-top: 76px; }

/* ---------------------------------------------------------------------------
   6. PROCESS — THE CONTENTS STRIP                                 volume 2
   The quietest thing between the exhibits and the price stack, and the only
   horizontal object in Act III. Four 64px discs that lit up in var(--grad) are
   now four 12px ordinals; the scrubbed rail is gone so the Thread is the only
   scroll-linked light on the page. No hover: a .pstep contains no link and no
   button, so a hover state on it was decoration pretending to be affordance.
   ------------------------------------------------------------------------ */
#process .proc-index { max-width: 1160px; }
#process .proc-head  { position: static; max-width: 780px; margin-bottom: 40px; }

#process .proc-steps {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: 1px solid var(--hairline);
}
#process .pstep {
  display: block; background: none; border-top: 0;
  border-left: 1px solid var(--hairline);
  padding: 26px 28px 0 26px;
}
#process .pstep:first-child { border-left: 0; padding-left: 0; }
#process .pnum {
  display: block; width: auto; height: auto;
  background: none; border-radius: 0; box-shadow: none; transform: none;
  font-size: 12px; font-weight: 600; letter-spacing: .1em;
  color: var(--accent-mid); margin-bottom: 12px;
}
#process .pbody h4 {
  font-size: 21px; font-weight: 600; color: var(--text);
  margin-bottom: 8px; transform: none;
}
#process .pbody p {
  font-size: 16px; font-weight: 300; line-height: 1.5;
  color: var(--muted); opacity: 1; transform: none;
}

/* ---------------------------------------------------------------------------
   7. FIX / REBUILD / GROW — THE QUOTE SHEETS         volume 5  (LOUDEST)
   Three levers, no new string, no new number:
     1. the amount is the largest type on the page after the h1
     2. the price sits in its own ruled column — a total on a quote
     3. "Most common", a string ALREADY IN THE DOM, is drawn instead of
        asserted: that one sheet inverts to --ink.
   It is an OBJECT, not a band: max-width 1080px, centred, never touching the
   viewport edge. DIRECTION-v19 §2's "one dark band in Act III" holds — #system
   is still the only dark band — and ink now marks the three places on the page
   that ask for a decision and nowhere else.
   ------------------------------------------------------------------------ */
#ways .price-card { max-width: 1080px; padding: 48px 52px; }
#ways .pc-amt     { font-size: clamp(50px, 4.8vw, 76px); letter-spacing: -.03em; }
#ways .pc-per     { font-size: 20px; }

@media (min-width: 1181px) {
  #ways .price-card { grid-template-columns: 1.4fr .8fr; gap: 0; }
  #ways .pc-side    { padding-left: 48px; border-left: 1px solid var(--hairline); }
}

/* THE INK SHEET — every token re-pointed. On --ink: #fff 18.28:1,
   --metal-lite 13.50:1, --silver 10.96:1. --accent would be 1.86:1, which is
   why not one of these inherits. */
#ways .price-card.is-featured {
  --focus: var(--metal-lite);
  background: var(--ink); border-color: var(--ink); color: #fff;
  box-shadow: 0 30px 72px -38px rgba(20, 30, 70, .78);
}
#ways .price-card.is-featured:hover {
  border-color: var(--ink);
  box-shadow: 0 34px 80px -32px rgba(20, 30, 70, .86);
}
#ways .price-card.is-featured .pc-for,
#ways .price-card.is-featured .pc-list li      { color: var(--metal-lite); }
#ways .price-card.is-featured:hover .pc-list li { color: #fff; }   /* 13.50 -> 18.28 */
#ways .price-card.is-featured .pc-list li      { border-top-color: rgba(255, 255, 255, .16); }
#ways .price-card.is-featured .pc-list li::before { border-color: var(--silver); }
#ways .price-card.is-featured .pc-from,
#ways .price-card.is-featured .pc-alt,
#ways .price-card.is-featured .pc-per,
#ways .price-card.is-featured .pc-note         { color: var(--silver); }
#ways .price-card.is-featured .pc-amt,
#ways .price-card.is-featured:hover .pc-amt    { color: #fff; }    /* hover holds, never drops */
#ways .price-card.is-featured .pc-side         { border-left-color: rgba(255, 255, 255, .16); }
/* The covered-card veil is a DARKEN — invisible on ink. Same --cover value,
   same rAF write, no JS change: this one card recedes by being LIGHTENED.
   #fbfbfc at .13 over --ink measures #313337; white on it is 12.66:1. */
#ways .price-card.is-featured::before { background: #fbfbfc; }
/* the cursor spotlight is a dark wash; on ink it does nothing */
#ways .price-card.is-featured::after {
  background: radial-gradient(280px circle at var(--mx, 50%) var(--my, 50%),
              rgba(255, 255, 255, .07), transparent 62%);
}

/* ---------------------------------------------------------------------------
   8. ONE ACCOUNTABLE TEAM — THE CERTIFICATE                       volume 3
   The only centred section on the site and the only 2x2 cross-ruled plate — a
   full stop between the two loudest sections. #team-scoped throughout because
   /about/ and /services/seo/ still ship .team-panel, .tp-line and .tp-rows.
   INTEGRATOR: the chapter marker is NOT centred and is NOT inside .team-seal.
   Agent B draws the Thread's node on the eyebrow's own left edge, so an eyebrow
   inside a 960px centred box would drag the node 144px off the line. The
   eyebrow stays on the container rail; the certificate under it is centred.
   ------------------------------------------------------------------------ */
#team .team-seal        { max-width: 960px; margin: 0 auto; text-align: center; }
#team .team-copy .lead  { margin: 18px auto 0; max-width: 60ch; }
#team .tp-head          { margin: 56px 0 16px; }

#team .tp-rows {
  display: grid; grid-template-columns: 1fr 1fr;
  padding-left: 0; text-align: left;
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
}
#team .tp-row                  { display: block; border-top: 0; }
#team .tp-row:nth-child(odd)   { padding: 26px 28px 26px 0; border-right: 1px solid var(--hairline); }
#team .tp-row:nth-child(even)  { padding: 26px 0 26px 28px; }
#team .tp-row:nth-child(-n+2)  { border-bottom: 1px solid var(--hairline); }
#team .tp-k   { display: block; min-width: 0; font-size: 21px; margin-bottom: 6px; }
#team .tp-d   { display: block; font-size: 16px; }
#team .tp-arw { display: none; }   /* aria-hidden decoration; an inline arrow in a
                                      grid cell points at nothing */
/* the base rule recedes un-hovered rows to --dot-mute; on a cross-ruled plate
   that reads as a rendering fault, not as a recede */
#team .tp-rows:hover .tp-row { border-color: var(--hairline); }

/* the four promises become a caption strip, not a fifth stacked list */
#team .check-list {
  margin-top: 40px; display: flex; flex-wrap: wrap;
  justify-content: center; gap: 12px 34px;
}
#team .check-row   { align-items: center; gap: 10px; }
#team .check-row p { font-size: 16px; }

#team .badge-wrap { margin: 48px auto 0; }
#team .team-cta   { margin-top: 26px; }

/* ---------------------------------------------------------------------------
   9. TESTIMONIALS — THE TESTIMONY                                 volume 4
   The page's only third-party proof was three identical 380px cards you had to
   lean in to read. It is now the largest body type in Act III at full measure,
   with the attribution and the five stars in a ruled right rail — three
   stacked ★★★★★ down one column is the mark the section was missing.
   The volume is TYPE. No card, no border, no shadow, no new asset.
   ------------------------------------------------------------------------ */
#reviews .review-grid {
  display: grid; grid-template-columns: 1fr; gap: 0;
  border-top: 1px solid var(--hairline);
}
#reviews .review {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(200px, 280px);
  grid-template-areas: "mark meta" "quote meta";
  gap: 0 56px; align-items: start;
  padding: 44px 0;
  background: none; border: 0; border-bottom: 1px solid var(--hairline);
  border-radius: 0; box-shadow: none; transform: none;
}
#reviews .review:hover { transform: none; box-shadow: none; border-color: var(--hairline); }
#reviews .review::after { display: none; }   /* spotlight needs a card to sit in */

#reviews .rv-mark {
  grid-area: mark; font-size: 46px; line-height: .55;
  color: var(--silver); margin-bottom: 14px;
}
#reviews .review:hover .rv-mark { color: var(--accent); }
#reviews .review blockquote {
  grid-area: quote; flex: none;
  font-size: clamp(22px, 2.1vw, 30px); font-weight: 300;
  line-height: 1.3; letter-spacing: -.012em; color: var(--text);
}
#reviews .review figcaption {
  grid-area: meta; align-content: start; gap: 6px;
  padding: 6px 0 0 40px;
  border-top: 0; border-left: 1px solid var(--hairline);
}
#reviews .rv-who { font-size: 18px; }
#reviews .rv-src { font-size: 16px; }

/* ---------------------------------------------------------------------------
   10. FAQ — THE REGISTER                                volume 1 (QUIETEST)
   The h2 is the FIRST ROW of the list, not a sticky column beside it — the
   only place on the page where the heading sits inside the component it
   introduces, which is what makes it unmistakable while also making it the
   smallest. The 40px bordered disc is gone: the +/x glyph is the indicator, at
   13.36:1 where WCAG 1.4.11 asks 3:1. #faq-scoped — /services/seo/ also ships
   .faq-row and must not move.
   ------------------------------------------------------------------------ */
#faq .faq-register { max-width: 900px; }
#faq .faq-head {
  position: static; padding-bottom: 24px;
  border-bottom: 1px solid var(--hairline);
}
#faq .faq-list  { border-top: 0; }
#faq .faq-q     { font-size: 19px; font-weight: 500; padding: 20px 0; gap: 20px; }
#faq .faq-ico   { width: 22px; height: 22px; border: 0; background: none; }
#faq .faq-q:hover .faq-ico,
#faq .faq-q:focus-visible .faq-ico { background: none; border-color: transparent; }
#faq .faq-q:active .faq-ico        { background: none; }
/* open state goes DARKER, not to --accent: rest 13.36 -> open 16.17 */
#faq .faq-row.is-open .faq-ico     { border-color: transparent; color: var(--on-metal); }
#faq .faq-a-inner   { padding: 0 0 26px; max-width: 74ch; }
#faq .faq-a-inner p { font-size: 16px; }

/* ---------------------------------------------------------------------------
   RESPONSIVE TAIL
   ------------------------------------------------------------------------ */
@media (max-width: 1180px) {
  #results .exhibits { gap: 72px; }
  #results .exhibit  { gap: 0 40px; }
  #reviews .review   { gap: 0 36px; grid-template-columns: minmax(0, 1fr) minmax(180px, 240px); }
  /* INTEGRATOR: the file already narrows .price-card here; an ID rule beats it,
     so the same value has to be restated or the card keeps desktop padding. */
  #ways .price-card  { padding: 38px 34px; }
}

@media (max-width: 1023px) {
  #results .exhibit { grid-template-columns: 1fr; gap: 26px 0; }
  #results .exhibit:nth-child(even) > .exh-shot,
  #results .exhibit:nth-child(even) > .exh-copy { grid-column: 1; grid-row: auto; }
  #results .exh-shot { position: static; }

  #process .proc-steps { grid-template-columns: 1fr 1fr; }
  #process .pstep,
  #process .pstep:first-child   { border-left: 0; padding: 24px 24px 24px 0; }
  #process .pstep:nth-child(even) { padding: 24px 0 24px 24px; border-left: 1px solid var(--hairline); }
  #process .pstep:nth-child(n+3)  { border-top: 1px solid var(--hairline); }
}

@media (max-width: 900px) {
  #results .exhibits { gap: 56px; }

  #reviews .review {
    grid-template-columns: 1fr;
    grid-template-areas: "mark" "quote" "meta";
    gap: 0; padding: 34px 0;
  }
  #reviews .review figcaption {
    padding: 22px 0 0; margin-top: 22px;
    border-left: 0; border-top: 1px solid var(--hairline);
  }

  #team .team-seal       { text-align: left; }
  #team .team-copy .lead { margin-left: 0; }
  #team .tp-rows         { grid-template-columns: 1fr; }
  #team .tp-row,
  #team .tp-row:nth-child(odd),
  #team .tp-row:nth-child(even)  { padding: 22px 0; border-right: 0; border-bottom: 1px solid var(--hairline); }
  #team .tp-row:last-child       { border-bottom: 0; }
  #team .check-list      { justify-content: flex-start; }
  #team .badge-wrap      { margin-left: 0; }
}

@media (max-width: 620px) {
  #results .exh-lines .case-line { grid-template-columns: 1fr; gap: 6px; padding: 16px 0; }

  #process .proc-steps { grid-template-columns: 1fr; }
  #process .pstep,
  #process .pstep:nth-child(even) { padding: 20px 0; border-left: 0; }
  #process .pstep:nth-child(n+2)  { border-top: 1px solid var(--hairline); }

  #ways .price-card { padding: 30px 24px; }
  #ways .pc-amt     { font-size: 44px; }

  #reviews .review blockquote { font-size: 21px; }
}

/* ---------------------------------------------------------------------------
   REDUCED MOTION
   Everything in this block is already static — no keyframes, no scrub, no
   scroll listener. The one call to make is the sticky screenshot, which
   follows .proc-head / .faq-head, both of which the file already un-sticks.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  #results .exh-shot { position: static !important; }
}
/* ===== end ============================================================== */

/* ===== v19 CONVERSION — AGENT E ===== */
/* Appended after the client-work block. That block's "MUST BE LAST" note is about
   its OWN responsive / reduced-motion tails winning over earlier rules — nothing
   below touches #results / #process / #ways / #team / #reviews / #faq internals,
   and everything below needs to sit after the 1023/900/620px and reduced-motion
   blocks to win. */

/* ---------------------------------------------------------------------------
   1. DRAWER = a real dialog.
   Was a 100%-wide strip pinned at top:86px with no scrim, no scroll lock, no
   focus trap and no outside-click target — and the nav can slide away
   (.is-hidden) while it is open, taking the only close control with it.
   ------------------------------------------------------------------------ */
html.drawer-open, html.drawer-open body { overflow: hidden; }
/* the burger is the only way back out — it may never leave while the panel is up */
.drawer-open .nav.is-hidden { transform: none; }
/* and a keyboard user inside the header keeps their landmark, drawer or not */
.nav.is-hidden:focus-within { transform: none; }

.drawer-grp { display: flex; flex-direction: column; gap: 16px; }
.drawer-sep { display: block; height: 1px; background: var(--hairline); }

/* ---------------------------------------------------------------------------
   1b. NAV LEGIBILITY OVER THE INK BAND.        [INTEGRATOR ADDITION]
   The fixed shell now passes over #system (--ink). With JS running that is safe:
   .is-stuck is set at y > 80 and #system starts far below that, so the shell is
   always wearing its rgba(252,252,252,.84) plate there.
   But .is-stuck is a JS CLASS. With JS blocked, or after the .js-fallback
   watchdog fires, it is never added, the shell stays transparent, and --muted
   #4e535b over --ink #131519 measures 1.21:1 — the nav effectively disappears
   for two full viewports. So the plate becomes unconditional in exactly the two
   gates where the class cannot arrive. Same declarations as .nav.is-stuck
   .nav-shell, so there is one plate on the page, not two.
   ------------------------------------------------------------------------ */
html:not(.js-enabled) .nav-shell,
.js-fallback .nav-shell {
  background: rgba(252, 252, 252, .84);
  -webkit-backdrop-filter: blur(16px);
          backdrop-filter: blur(16px);
  border-color: var(--hairline);
  box-shadow: 0 12px 34px -22px rgba(20, 30, 70, .38);
}

/* ---------------------------------------------------------------------------
   2. STICKY CTA — parked = unfocusable.
   translateY(130%) leaves a fully tabbable link 130% below the fold. visibility
   is animatable as a discrete step, so the slide still plays on the way in and
   the control only becomes reachable once it is on screen. Nothing is
   display:none'd, so no copy leaves the DOM.
   ------------------------------------------------------------------------ */
.sticky-cta.is-on { visibility: visible; }

/* The drawer PRECEDES .sticky-cta in the body (nav.html vs tail.html), so a
   sibling combinator reaches it — no :has(), no JS. (0,2,1) beats
   `.sticky-cta.is-on` at (0,2,0). */
.drawer:not([hidden]) ~ .sticky-cta { transform: translateY(130%); visibility: hidden; }

/* ---------------------------------------------------------------------------
   3. FORM — error, prefill and submitting states.
   No new copy: the boundary and aria-invalid carry the error, the browser's own
   validation bubble carries the sentence.
   ------------------------------------------------------------------------ */
.field.is-err input, .field.is-err textarea, .field.is-err select { border-color: #b3261e; }
.field.is-err label { color: var(--text); }
.choices.is-err .choice { border-color: #b3261e; }
/* every hover on this site ADDS definition — 6.53:1 -> 8.94:1, never the reverse */
.field.is-err input:hover, .field.is-err textarea:hover, .field.is-err select:hover,
.choices.is-err .choice:hover { border-color: #8e1e18; }
/* must be (0,3,0): `.field.is-err input` (0,2,0) sits after `.field input:focus`
   in the cascade and would otherwise eat the focus treatment entirely */
.field.is-err input:focus, .field.is-err textarea:focus, .field.is-err select:focus {
  border-color: #8e1e18;
  box-shadow: 0 0 0 4px rgba(179, 38, 30, .16);
}

/* the answer carried in from a price card. --accent (9.83:1 on the white panel,
   well over the 3:1 control floor) — deliberately the loudest boundary in the
   form, because this is the field the visitor must SEE has been answered. */
.field.is-prefilled select { border-color: var(--accent); }

/* the failure line's address becomes reachable. Colour alone cannot distinguish
   a link from its surrounding text, so it is underlined. */
.form-msg a { text-decoration: underline; text-underline-offset: 2px; }
.form-msg a:hover, .form-msg a:focus-visible { color: #8e1e18; }

/* SUBMITTING. main.js now sets aria-disabled instead of disabled: `disabled`
   removes the button from the a11y tree mid-request and dumps focus on <body>.
   Same visual as the existing :disabled block — the light goes out of the
   MATERIAL, never out of the label (12.38 -> 6.54:1, still over AA), and the
   label says "Sending…" in words. */
.btn-primary[aria-disabled="true"],
.btn-primary[aria-disabled="true"]:hover,
.btn-primary[aria-disabled="true"]:focus-visible,
.btn-primary[aria-disabled="true"]:active {
  color: var(--muted);
  background: #e9ecf1;
  box-shadow: none;
  transform: none;
}
.btn-primary[aria-disabled="true"]::before {
  background-image: none;
  background-color: #e9ecf1;
  box-shadow: 0 0 0 1px inset var(--silver);
}
.btn-primary[aria-disabled="true"]::after,
.btn-primary[aria-disabled="true"]:hover::after { opacity: 0; }
.btn-primary[aria-disabled="true"] .arw::before { transform: none; opacity: 1; }
.btn-primary[aria-disabled="true"] .arw::after  { opacity: 0; }

/* ---------------------------------------------------------------------------
   4. THE THREAD'S TERMINUS.      [INTEGRATOR: ADAPTED TO THE LIVE GEOMETRY]
   Agent E specified its own line + node at x = max(9px, calc(--pad - 14px)).
   The Thread already runs INSIDE .cta-band (Agent B: [data-thread] > .cta-band
   ::before, top -80px / bottom 100px, on --thr-x), so E's own section 6 ask 2
   and Risk 6 apply: the ::before LINE is DROPPED — a second silver line ~30px
   away from the first would have kinked the page's signature element at its
   last seam — and only the NODE is kept, re-bound to the live tokens:

     line centre  = --thr-x + 5.5px   (an 11px track box painting a 1px line at 50%)
     cap centre y = --thr-tail + 5px  (Agent B's 11x10 radial pinned at 50% 100%)

   so a 14px node is `left: --thr-x - 1.5px` and `bottom: --thr-tail - 2px`,
   exactly concentric with the track's own 10px end-cap, which it covers. E's own
   numbers (bottom: 93px) came out of the same 100px padding and land within 5px;
   the concentric value is used so there is ONE object at the terminus.

   z-index 3, not E's 1: the track is z-index 2, so at 1 the cap would paint over
   the node instead of under it.

   Scoped to [data-thread] — cta.html is shared with 8 interior pages that have
   no Thread — and dropped below 1024px with the rest of the Thread.

   The node is the marker family's "live" recipe with the ring re-pointed to
   --metal-lite: --accent measures 1.86:1 on ink and would simply not be there.
   Static in every state: no animation, no JS, fully lit with JS off and under
   reduced motion. aria-invisible by construction (a pseudo-element).
   ------------------------------------------------------------------------ */
[data-thread] > .cta-band::after {
  content: ''; position: absolute; z-index: 3; pointer-events: none;
  left: calc(var(--pad) - var(--thr-inset) - 1.5px);   /* fallback, no max() */
  left: calc(var(--thr-x) - 1.5px);
  bottom: calc(var(--thr-tail, 100px) - 2px);
  width: var(--dot-md); height: var(--dot-md);
  border-radius: var(--r-round);
  background-image: var(--grad);
  box-shadow: inset 0 0 0 var(--dot-ring) var(--metal-lite),
              0 0 0 3px rgba(216, 222, 230, .16);
}
@media (max-width: 1023px) {
  [data-thread] > .cta-band::after { display: none; }
}

/* ---------------------------------------------------------------------------
   5. THE ARRIVAL — one quiet move.
   Metal is reserved for the primary CTA, the Thread's travelling light and the
   active marker (DIRECTION section 5), so the light coming to rest IN the submit
   button is not a new idea, it is the same object arriving. Opacity only, on a
   decorative sheen layer that can only ADD white — the --on-metal label's
   contrast rises with it (worst composited case 13.0:1, see :root), never falls.

   Fail-open: lit. JS off -> .55. Reduced motion -> .55, no transition.
   Ordering matters: hover/press are restated here at (0,3,0) because this block
   is appended last and would otherwise outrank .btn-primary:hover::after.
   ------------------------------------------------------------------------ */
.cta-band .cta-arrive::after { opacity: .55; }
.js-enabled .cta-band .cta-arrive:not(.is-arrived)::after { opacity: 0; }
.cta-band .cta-arrive:hover::after,
.cta-band .cta-arrive:focus-visible::after { opacity: 1; }
.cta-band .cta-arrive:active::after { opacity: .3; transition-duration: .12s; }

/* ---------------------------------------------------------------------------
   6. RESPONSIVE TAILS
   ------------------------------------------------------------------------ */
@media (max-width: 900px) {
  /* 108px of reserved space at the foot of every page was unconditional — with
     JS off the bar is [hidden] and never appears, so it was dead air. */
  .footer { padding-bottom: 72px; }
  .js-enabled .footer { padding-bottom: 108px; }
}

@media (max-width: 620px) {
  /* THE BAR STACKS instead of deleting its own reassurance. `display: none` on
     .sticky-cta-txt dropped "One business day · no automated pitch" from exactly
     the audience that needs it, and the ellipsis truncated it at 621–900px.
     Measured at 390px: 18 + 16 + 40 + 8 + 16 = 98px + safe-area — a standard
     mobile action bar, and the reassurance now rides ON the action. */
  .sticky-cta-txt { display: grid; }
  .sticky-cta-txt span { white-space: normal; overflow: visible; text-overflow: clip; }
  .sticky-cta-in {
    flex-direction: column; align-items: stretch; gap: 8px;
    padding: 10px 14px 12px;
    border-radius: 26px;
  }
  .sticky-cta .btn-sm { width: 100%; justify-content: center; }

  /* E's 620px terminus variant is dropped: the Thread — and with it the
     terminus — does not exist below 1024px (Agent B, B6). */
}

/* ---------------------------------------------------------------------------
   7. REDUCED MOTION — everything here is already static; these two pin the
   states that would otherwise depend on a JS class arriving.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .js-enabled .cta-band .cta-arrive::after { opacity: .55; }
  .sticky-cta, .drawer:not([hidden]) ~ .sticky-cta { transition: none !important; }
}
/* ===== end ===== */

/* ===== v19c: the hero proof wall was REMOVED in v19b =====================
   The owner's call: "the landing hero should be without client but focus on
   us." The four screenshots are untouched in #results at 662px with their full
   descriptive alt text; only the hero's copy of them is gone, and with it ~392KB
   (192KB of it eager) off the LCP screen.

   Everything that styled .hero-proof / .hp is deleted here — 27 dead rules and
   three whole media queries. What survives is only the vertical rhythm the wall
   left behind, which the hero still uses. */
.hero { padding-top: 112px; }
.hero .trust-strip { padding-top: 24px; }
.hero-thread-cap { margin-top: 20px; }


/* The wall was landing ~870px down: at 1600x900 only its top edge was on the
   first screen, which is the exact problem it exists to solve. Two levers, no
   type shrunk: the h1 measure goes 19ch -> 27ch so a 53-character headline sets
   in TWO lines instead of three (it also spans nearly the full container at
   1600, which is more editorial confidence, not less), and the stack between
   the lead and the wall gives back its slack. Net: ~150px, so the wall opens on
   the first screen at 900px of viewport. */
[data-thread] .hero-title { max-width: 27ch; margin: 0 0 26px; }
.hero { padding-top: 96px; }
.hero .lead { margin-bottom: 28px; }
.hero-cta { margin-bottom: 32px; }
.hero .trust-strip { padding-top: 20px; }
.hero-thread-cap { margin-top: 14px; }

/* ===== v19a HERO — EVIDENCE FIRST, AND THE JOURNEY ======================
   Three reviewers (visual design / conversion / motion) read v19's hero against
   the owner's proposal to replace the proof wall with four drawn windows —
   search engine, website, form, booking — walking a mirrored L. All three
   returned the same verdict and this block is what survived it.

   KEPT: the four real client sites. They are the only evidence and the only
   colour above the fold, and DIRECTION-v19 section 5 already deleted one hero
   diagram for having to be read before it meant anything.

   BUILT: the L, as an annotation UNDER the wall rather than instead of it, and
   with five stops rather than four — Search / Website / Form / Booking stops the
   story at Booking, which is two stages short of what #system sells and directly
   contradicts #results ("We do not disappear after the website launches").

   APPEND AT EOF, and nothing may follow: this block carries its own responsive
   and reduced-motion tails.
   ------------------------------------------------------------------------ */

[data-thread] {
  /* the drop between the journey's two rails */
  --hj-h: 72px;
  /* --ht-offset reaches the spine from inside .hero-copy, which carries
     --hero-indent. The trust strip and the caption are now .container children
     and have no indent to cancel, so they need their own offset. Keeping them as
     two separate variables is what stops the 26px indent below 900px from being
     silently applied twice. */
  --ht-offset-c: calc(var(--thread-x) - var(--pad));
}

/* ---------- the CTA row ---------------------------------------------------
   "See client work" pointed at #results while #results' evidence was already on
   the screen 200px below the button — a control with nowhere to take you, given
   the same weight as the one action this page wants. Demoted to a text link. */
.hero-cta { align-items: center; gap: 20px; }
.hero-link {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 15px; font-weight: 500; letter-spacing: .01em;
  color: var(--accent);                       /* 9.5:1 on --bg */
  text-decoration: none;
  padding-bottom: 3px;
  border-bottom: 1px solid var(--hairline);
  transition: border-color .3s var(--e-out);
}
.hero-link:hover { border-bottom-color: var(--accent); }   /* hover ADDS definition */
.hero-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 2px; }

/* ---------- the credit ----------------------------------------------------
   One verified third-party review, beside the CTA, because the visitor arriving
   from an Upwork proposal is deciding whether we are real — and no claim we make
   about ourselves answers that as fast as a review someone else wrote.
   aria-hidden: the identical strings are in #reviews, announced once. */
.hero-credit { margin: 22px 0 0; max-width: 60ch; padding-left: var(--hero-indent); }
.hc-q {
  display: block;
  font-size: 15px; line-height: 1.55; font-style: italic;
  color: var(--muted);                        /* 8.0:1 on --bg */
}
.hc-src {
  display: block; margin-top: 7px;
  font-size: 11px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--tertiary);                     /* 5.1:1 on --bg — small type, one line */
}

/* ---------- the trust strip, demoted -------------------------------------
   It moved out of .hero-copy and now sits under the wall. Four claims we make
   about ourselves no longer stand between the headline and the four sites that
   prove them. Its border-top still springs from the spine; only the variable it
   springs from changed. The bottom margin is what RESERVES the journey's band,
   so the absolutely-positioned rails below can never overlap the strip and can
   never cause a reflow when they animate. */
.hero .trust-strip {
  margin: 30px 0 0 var(--ht-offset-c);
  padding-left: calc(var(--ht-offset-c) * -1);
  padding-top: 20px;
}

/* THE RESERVE IS PADDING, NOT A MARGIN. Measured: as a margin-bottom on the last
   flow child it COLLAPSED straight out of .container (no bottom padding, no
   border, nothing to stop it), .container's padding box ended at the strip's own
   bottom edge, and the journey — pinned to bottom: 0 — was drawn 72px UP THROUGH
   the strip's text. Padding on the container cannot collapse, and bottom: 0
   resolves against the padding box, so the 30px gap is now guaranteed. */
.hero > .container { padding-bottom: calc(var(--hj-h) + 30px); }

/* ---------- THE JOURNEY ---------------------------------------------------
   Three legs and an exit. It has no left side of its own: the spine IS its left
   side, which is why .hero-thread now stops at var(--hj-h) instead of running
   past the hero's bottom edge. Both are pinned to --thread-x, so the return leg
   lands on the spine's own pixel at every width — collinear by construction, in
   the same way the hero segment is collinear with #how's ::before.

   Absolutely positioned, fixed height, both ends bound to box edges: nothing
   here can reflow, and nothing here can cost CLS on the LCP screen. */
.hero-journey {
  position: absolute; z-index: 0; pointer-events: none;
  left: calc(var(--thread-x) - var(--thread-w) / 2);
  right: var(--pad);
  bottom: 0;
  height: var(--hj-h);
}
.hj-track, .hj-fill { position: absolute; border-radius: var(--thread-w); }
.hj-track { background: var(--hairline); }                 /* 1.37:1 — decorative */
.hj-fill  { background: var(--accent); box-shadow: 0 0 6px rgba(61, 68, 78, .22); }

/* leg A — out along the top, left to right, carrying the five stops */
.hj-t-a { left: 0; top: 0; right: 0; height: var(--thread-w); }
.hj-a   { left: 0; top: 0; width: 100%; height: var(--thread-w);
          transform-origin: left center;  transform: scaleX(1); }
/* leg B — the drop at the far right */
.hj-t-b { right: 0; top: 0; width: var(--thread-w); height: 100%; }
.hj-b   { right: 0; top: 0; width: var(--thread-w); height: 100%;
          transform-origin: top center;   transform: scaleY(1); }
/* leg C — the RETURN. It carries no stops: the stages are progress and they run
   left to right on leg A; this leg is the light coming home, not a fifth step. */
.hj-t-c { left: 0; bottom: 0; right: 0; height: var(--thread-w); }
.hj-c   { left: 0; bottom: 0; width: 100%; height: var(--thread-w);
          transform-origin: right center; transform: scaleX(1); }
/* leg D — back on the spine and out of the hero. --sec-gap is exactly .hero's
   bottom padding, so this ends on the hero's border edge and #how's thread picks
   up from the same x with no seam. */
.hj-t-d { left: 0; top: 100%; width: var(--thread-w); height: var(--sec-gap); }
.hj-d   { left: 0; top: 100%; width: var(--thread-w); height: var(--sec-gap);
          transform-origin: top center;   transform: scaleY(1); }

/* THE DEFAULT IS FULLY DRAWN. Exactly as with the spine, the undrawn state is
   the only thing behind the JS gate, and the gate releases itself. */
.js-enabled .hero-journey:not([data-walked]) .hj-a,
.js-enabled .hero-journey:not([data-walked]) .hj-c { transform: scaleX(0); }
.js-enabled .hero-journey:not([data-walked]) .hj-b,
.js-enabled .hero-journey:not([data-walked]) .hj-d { transform: scaleY(0); }

/* ---------- the five stops ------------------------------------------------
   The five stage names already in #system, in the same order. The LABELS never
   animate — text-safety: every string on this page resolves to opacity 1 in
   every state, and a word that fades in is a word that can be caught mid-fade.
   Only the dots light, and they are hairline rings that fill with --accent, NOT
   metal: the direction allows exactly three metal moments and all three are
   already spoken for. */
.hj-stops {
  position: absolute; inset: 0;
  display: flex; justify-content: space-between;
  margin: 0; padding: 0; list-style: none;
}
.hj-stops li {
  position: relative; flex: none; width: 0;
  display: flex; flex-direction: column; align-items: center;
}
.hj-stops li:first-child { align-items: flex-start; }
.hj-stops li:last-child  { align-items: flex-end; }
.hj-dot {
  position: relative; flex: none;
  width: 12px; height: 12px; margin-top: -5px;
  border-radius: var(--r-round);
  border: 1.5px solid var(--hairline);
  background: var(--bg);
}
.hj-dot::after {
  content: ''; position: absolute; inset: 2px;
  border-radius: inherit; background: var(--accent);
  opacity: 1;                                  /* LIT = the default */
}
.js-enabled .hero-journey:not([data-walked]) .hj-dot::after { opacity: 0; }
.hj-lbl {
  margin-top: 11px; white-space: nowrap;
  font-size: 11px; font-weight: 500; letter-spacing: .09em;
  text-transform: uppercase;                   /* small type — the one thing caps still mean */
  color: var(--tertiary);                      /* 5.1:1 on --bg */
}

/* ---------- the light -----------------------------------------------------
   The SAME object as the spine's head, sharing one recipe (see .ht-light) so the
   page still has exactly one travelling light. .ht-light finishes at the top-left
   corner and this one starts there, on the same pixel, in the same frame — a
   hand-off, not a second light. Only the origin is re-pointed: .ht-light centres
   on a 2px box with left: 50%, and this one lives in a wide box, so it centres on
   the same line by half a thread instead. */
/* .js-enabled is on the selector because the shared recipe sets `left: 50%` at
   that specificity — measured: without it the head started at the RAIL's centre,
   777px right of the spine, and the walk overshot the right corner by half the
   rail. Specificity, not !important: the recipe above is meant to be re-pointed. */
.js-enabled .hj-light { top: 0; left: calc(var(--thread-w) / 2); }

/* One pass, then gone. Percentages are the path's own px ratios at 1600
   (1224 + 72 + 1224 + 88), so the light holds a near-constant velocity through
   all four legs. --hj-w is the single measurement JS makes for this block. */
.hero-journey.is-walking .hj-a { animation: hjDrawX .56s linear 1.75s forwards; }
.hero-journey.is-walking .hj-b { animation: hjDrawY .04s linear 2.31s forwards; }
.hero-journey.is-walking .hj-c { animation: hjDrawX .56s linear 2.35s forwards; }
.hero-journey.is-walking .hj-d { animation: hjDrawY .04s linear 2.91s forwards; }
/* THE HAND-OFF. hjRun starts at 1.75s — the exact millisecond the spine's strike
   ends, at the exact pixel it ends on. hjFlash runs on its OWN clock, starting
   140ms EARLIER, so this head fades up over precisely the span htFlash fades the
   spine's head down, both sitting on the same coordinate. Measured before the
   split: the spine went dark at 1.75s and this one lit at 1.79s, and the corner
   read as one light dying and another arriving. It is one light. */
.hero-journey.is-walking .hj-light {
  animation: hjRun 1.20s linear 1.75s forwards, hjFlash 1.34s linear 1.61s forwards;
}
.hero-journey.is-walking .hj-stops li:nth-child(1) .hj-dot::after { animation: hjDot .26s var(--e-out) 1.75s forwards; }
.hero-journey.is-walking .hj-stops li:nth-child(2) .hj-dot::after { animation: hjDot .26s var(--e-out) 1.89s forwards; }
.hero-journey.is-walking .hj-stops li:nth-child(3) .hj-dot::after { animation: hjDot .26s var(--e-out) 2.03s forwards; }
.hero-journey.is-walking .hj-stops li:nth-child(4) .hj-dot::after { animation: hjDot .26s var(--e-out) 2.17s forwards; }
.hero-journey.is-walking .hj-stops li:nth-child(5) .hj-dot::after { animation: hjDot .26s var(--e-out) 2.31s forwards; }

@keyframes hjDrawX { from { transform: scaleX(0) } to { transform: scaleX(1) } }
@keyframes hjDrawY { from { transform: scaleY(0) } to { transform: scaleY(1) } }
@keyframes hjDot   { from { opacity: 0 }          to { opacity: 1 } }
@keyframes hjRun {
  0%   { transform: translate3d(0, 0, 0) }
  47%  { transform: translate3d(var(--hj-w, 1100px), 0, 0) }
  50%  { transform: translate3d(var(--hj-w, 1100px), var(--hj-h), 0) }
  97%  { transform: translate3d(0, var(--hj-h), 0) }
  100% { transform: translate3d(0, calc(var(--hj-h) + var(--sec-gap)), 0) }
}
@keyframes hjFlash { 0% { opacity: 0 } 10%, 90% { opacity: 1 } 100% { opacity: 0 } }

/* ---------- the spine now ends AT the journey ----------------------------- */
.hero-thread { bottom: var(--hj-h); }

/* ---------- the caption ---------------------------------------------------
   Was floating between the trust strip and the wall, labelling nothing. It is
   now the journey's payoff line, sitting where the light rejoins the spine and
   leaves the hero. It is the one real string in this band, so it stays OUT of
   the aria-hidden block and keeps its place in the reading order. Absolute, so
   it takes no part in the flow and cannot move the wall. */
.hero-thread-cap {
  position: absolute;
  left: calc(var(--thread-x) + 16px);
  top: 100%; margin: 22px 0 0;
}

/* ---------- responsive ----------------------------------------------------
   Below 700 the journey is REMOVED, not shrunk. A three-leg path across a 390px
   column is a straight line with corners: the concept has no expression there,
   and the band it reserves is 100px of the exact fold budget the phone needs for
   the wall. The caption returns to the flow and the spine runs to the hero's
   bottom edge again, so the page below is byte-for-byte the v19 behaviour. */
@media (max-width: 900px) {
  .hero .trust-strip { padding-left: var(--hero-indent); }
  .hj-lbl { font-size: 10px; letter-spacing: .07em; }
}
/* ---------- THE PHONE FOLD -----------------------------------------------
   Measured on v19 at 390x844: the wall's top edge sat at y=1046 against an
   844px fold. Every phone visitor — the majority of this page's traffic — got
   the same evidence-free first screen the wall was built to abolish, 202px
   below where they could see it.

   Two moves, no type shrunk and no content removed:
     1. .container becomes a flex column so the CREDIT can take order: 3 and drop
        BELOW the wall. It is 70px of text standing between a phone user and the
        only proof on the screen; below 700 the screenshot answers "are they
        real" faster than the sentence does, and the sentence is still there,
        two thumb-lengths lower and unchanged.
     2. The hero's vertical rhythm gives back its slack — the same lever v19a
        used at desktop, applied where the fold is actually tight.
   Note flex items do not collapse margins, so every gap below is stated once
   and explicitly rather than inherited from a collapse that no longer happens. */
@media (max-width: 700px) {
  .hero-journey { display: none; }
  .hero > .container {
    display: flex; flex-direction: column;
    padding-bottom: 0;
  }
  .hero-copy       { order: 1; }
  .hero-credit     { order: 3; margin-top: 22px; }
  .trust-strip     { order: 4; }
  .hero-thread-cap { order: 5; }

  .hero { padding-top: 78px; }
  .hero-meta  { margin-bottom: 22px; }
  .hero .lead { margin-bottom: 24px; }
  .hero-cta   { margin-bottom: 0; }

  .hero-thread  { bottom: calc(var(--sec-gap) * -1); }
  .hero .trust-strip { margin: 26px 0 0 var(--ht-offset-c); }
  .hero-thread-cap {
    position: static;
    margin: 16px 0 0;
    padding-left: var(--hero-indent);
  }
}

/* ---------- the fail-open gates ------------------------------------------
   Same four, none depending on another: the CSS default is fully drawn, the JS
   sets data-walked on a timer regardless of what the animation does, .js-fallback
   force-draws after the head script's 3s watchdog, and reduced motion force-draws
   with no light at all. */
.js-fallback .hj-a, .js-fallback .hj-c { transform: scaleX(1) !important; }
.js-fallback .hj-b, .js-fallback .hj-d { transform: scaleY(1) !important; }
.js-fallback .hj-dot::after { opacity: 1 !important; }
.js-fallback .hj-light      { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  .hj-a, .hj-c { transform: scaleX(1) !important; }
  .hj-b, .hj-d { transform: scaleY(1) !important; }
  .hj-dot::after { opacity: 1 !important; }
  .hj-light { display: none !important; }
  .hero-journey.is-walking .hj-a,
  .hero-journey.is-walking .hj-b,
  .hero-journey.is-walking .hj-c,
  .hero-journey.is-walking .hj-d,
  .hero-journey.is-walking .hj-light,
  .hero-journey.is-walking .hj-dot::after { animation: none !important; }
}
/* ===== end ===== */

/* ===== v19c — ① THE SETTLE ==============================================
   One machined block in the hero's right side. Form and material from the UI
   spec, timing and light coupling from the motion spec, message from
   docs/BRIEF-hero-right-column.md. Read that brief before changing anything
   here: most of what looks like a free choice below is load-bearing.

   THE MEASUREMENT THAT SHAPED IT. The obvious build — reserve a 360x440 column
   for the object — was specified, simulated at every width from 1180 to 1920,
   and rejected. The h1 is 84px, so a reserved column does not cost the headline
   its second line; at 1200 and 1280 it costs a FOURTH, sets an editorial
   headline in ragged 320/462/577/387px lines, adds 176-287px of hero and pushes
   the journey rail off the fold. Affordable only above ~1400. So nothing is
   reserved and nothing moves: the hero already contained an empty room — right
   of the copy, below the headline, above the trust strip's rule, measured at
   363x324 at 1200 and 520x337 above 1460 — and the object moved into it.

   COSTS, all measured: no h1 change, no reflow, no CLS, no fold loss, no LCP
   delay, no request, no image, no string, no new colour.

   APPEND AT EOF and let nothing follow: this block carries its own responsive
   and reduced-motion tails.
   ====================================================================== */

:root {
  /* --e-out and --e-back both fail this object, for the same reason, and it is
     not the overshoot:
        --e-out  (.22,1,.36,1)   initial slope 4.55, no overshoot at all
        --e-back (.34,1.4,.64,1) initial slope 4.12, overshoot 5.3% of travel
     Both LAUNCH at full speed in frame one. That is correct for what they do —
     they drive 8-14px markers and card lifts over ~300ms as a REACTION TO AN
     INPUT, where an instant start is what makes a control feel responsive.
     This is not a reaction to an input. It is a mass arriving, and a mass
     accelerates. (And --e-back's 5.3% of a 28px rise is 1.5px, which does not
     survive DPR rounding on a 1x display — while the settle IS the concept.)

     .4,.1,.35,1.5 gives initial slope 0.25 — it accelerates out of the page —
     one overshoot of +10.8% of travel at 76% of the duration, and a 216ms
     damped return whose last half-pixel takes ~70ms, so the object visibly
     stops before the animation formally ends. Peak velocity 75px/s: six times
     slower than the next slowest moving thing in this hero (the spine's light
     at 457px/s). That number is what "heavy" means here.

     Not fixable by lengthening the travel: 28 -> 57px would make --e-back's
     5.3% land on 3px, and would turn a settling mass into a slide-in. Raise
     the curve, not the distance. */
  --e-settle: cubic-bezier(.4, .1, .35, 1.5);
}

[data-thread] {
  --settle-w:    320px;   /* = 363 (narrowest measured free gap, at 1200) - 43 air */
  --settle-h:    288px;   /* = 324 (shortest measured free band, at 1200) - 36 air */
  --settle-cham:  84px;   /* the chamfer leg, equal on both axes => exactly 45deg */
  --trust-h:      63px;   /* border 1 + padding-top 20 + one pill row 42. MEASURED
                             constant 1180-1920: the container's padding-box bottom
                             is always the trust strip's top + 165. */

  /* ---- THE ARRIVAL ----
     28px is already this hero's vertical unit — .hero-node's `top` and
     .hero .trust-strip's `padding-top` are both 28px. It is also 9.7% of the
     object's own height: enough that the shadow visibly compresses under it,
     small enough that it can never read as a card entering from off-canvas,
     which is what everything over ~40px reads as. */
  --settle-rise: 28px;
  --settle-t0:   .10s;    /* NOT 0s: one frame's grace so the arrival's first
                             visible frame is never also the frame that rasters
                             the layer, which sits inside the LCP window. */
  --settle-dur:  .90s;    /* at rest by 1.00s — 0.75s before the light leaves
                             the spine, so the catch's whole decay and the
                             journey's entire walk play over an object that is
                             already dead still. The stillness is what makes the
                             light legible; two things moving in the same half
                             of the screen would read as a loading state. */

  /* ---- THE CATCH ----
     Overwritten by initHeroThread()'s measure() from the SPINE'S OWN GEOMETRY.
     The strike is linear over the thread's height, so a fraction of that height
     is a fraction of the strike's 1.4s:
        t0  = the moment the light is level with the object's TOP edge
        dur = how long the light takes to pass the object's own height
     i.e. the facet is lit for exactly as long as the light is beside it, and
     settleCatch's 50% keyframe IS the light level with the object's centre.
     These defaults are the measured 1600x900 values, so a failed read degrades
     to a correct-looking catch rather than to none. */
  --settle-lit-t0:  976ms;
  --settle-lit-dur: 605ms;

  /* THE POINTER. Written by JS onto .settle-face, so the style recalc these two
     trigger is scoped to one element. Defaulted here so every gate resolves. */
  --settle-px: 0px;
  --settle-py: 0px;
}

/* BELOW 1200 THE OBJECT DOES NOT EXIST. Not shrunk, not moved behind the copy,
   not reinvented for a 390px column: a 320px mass needs 363px of clear room and
   1024 has 299. The phone hero is copy, CTA and one verified review, and that is
   the correct first screen — leave it alone. This is the base rule, so below
   1200 not one declaration in this file changes and the hero keeps exactly one
   declared layout breakpoint (900px). */
.settle { display: none; }

@media (min-width: 1200px) {

  /* ---- the mass ----------------------------------------------------------
     Absolutely positioned against .container, both anchors bound to box edges
     that cannot move: the container's content right edge, and its padding-box
     bottom. Nothing here can reflow and nothing here can cost CLS on the LCP
     screen — the same construction .hero-thread and .hero-journey already use,
     for the same reason. It is NOT a grid or flex item: the moment it is in
     flow, its box is in the h1's layout and CLS becomes possible.

     bottom = 72 + 30 + 63 = 165px puts the object's base EXACTLY on the trust
     strip's border-top. Verified at 1200 (647) and 1600 (669). That is the
     whole argument: the only horizontal rule in the hero leaves the Thread,
     runs 972px, and ends inside something solid.

     The cast shadow is .hp's recipe — the removed proof wall's cards — scaled
     up, with a contact layer, because this object TOUCHES the rule it sits on.
     The negative spread caps lateral reach at ~28px, so even at 1200, where the
     nearest glyph is 43px away, no text is ever composited against it. */
  .settle {
    display: block;
    position: absolute; z-index: 0;
    pointer-events: none;        /* aria-hidden decoration: it must never eat a
                                    click, and the pointer response listens on
                                    .hero, not here — it registers someone
                                    crossing the room, not being touched. */
    right:  var(--pad);
    bottom: calc(var(--hj-h) + 30px + var(--trust-h));
    width:  var(--settle-w);
    height: var(--settle-h);
    box-shadow: 0 2px   4px -2px  rgba(20, 22, 26, .22),
                0 30px 56px -28px rgba(20, 22, 26, .55);

    /* THE FINAL RESTING STATE IS THE DEFAULT. JS blocked, no class, no keyframe:
       the block is fully formed, opaque and perfectly still, with no code having
       run at all. Gate 1 of 4. */
    opacity: 1;
    transform: none;
  }

  /* ---- the face ----------------------------------------------------------
     clip-path cuts the whole subtree, so both pseudo-elements are chamfered for
     free. It also clips inset shadows to the BORDER box rather than the polygon,
     which is why every edge below is a background layer and not --metal-inset:
     an inset stack would leave the chamfer with no edge treatment at all.
     --metal-inset is the recipe for a LIGHT dome on a rounded control; this is a
     DARK block with a cut corner. Same light direction, different part.

     GEOMETRY CONSTANT behind the stop percentages: at 135deg the gradient line
     on a WxH box is (W+H)/1.4142 = (320+288)/1.4142 = 429.92px. The chamfer's
     inner edge lies 84*sin45 = 59.40px along it = 13.816%. All three chamfer
     stops derive from that one number. IF --settle-w OR --settle-h EVER CHANGES,
     recompute cham*0.7071 / ((W+H)*0.7071) and update stops 5, 6 and the catch.
     That is the price of an object that never scales — and it never scales. */
  .settle-face {
    position: absolute; inset: 0;
    clip-path: polygon(0 var(--settle-cham), var(--settle-cham) 0,
                       100% 0, 100% 100%, 0 100%);
    background-color: var(--accent-dk);        /* fallback body — never seen */
    background-image:
      /* 1 top edge, lit   */ linear-gradient(to bottom, rgba(255,255,255,.09) 0 1px, rgba(255,255,255,0) 1px),
      /* 2 left edge, lit  */ linear-gradient(to right,  rgba(255,255,255,.07) 0 1px, rgba(255,255,255,0) 1px),
      /* 3 bottom, cut     */ linear-gradient(to top,    rgba(0,0,0,.45) 0 2px, rgba(0,0,0,0) 2px),
      /* 4 right, cut      */ linear-gradient(to left,   rgba(0,0,0,.34) 0 2px, rgba(0,0,0,0) 2px),
      /* 5 the arris — the bright line where the bevel meets the face.
           MEASURED FIX: the first build put stops 5 and 6 at 13.3-14.4%, i.e.
           BEFORE the cut, which is exactly the triangle clip-path removes — the
           bevel was being painted into the corner that isn't there, and the
           object rendered as a plain slab with a cut corner and no chamfer. The
           visible material STARTS at 13.816%, so the bevel has to be a band just
           INSIDE that, and the arris sits at its inner boundary. */
      linear-gradient(135deg, rgba(255,255,255,0)   19.55%,
                              rgba(255,255,255,.34) 19.87%,
                              rgba(255,255,255,0)   20.25%),
      /* 6 the bevel — a 26px plane (6.05% of the 429.92px gradient line) running
           from the cut inwards. It is the one surface normal to this build's
           declared 135deg light, which is the whole reason the corner is cut. */
      linear-gradient(135deg, var(--gf-c)                13.82%,
                              #7c8492                    16.40%,
                              #6a7280                    19.80%,
                              rgba(106,114,128,0)        19.87%),
      /* 7 the mill — the surface is worked, not poured. Deliberately faint: at
           .022 over a 4px period it read as woven fabric rather than machined
           metal. Soft stops on a 5px period; a hard-stop version aliases at
           1.25x DPR. */
      repeating-linear-gradient(135deg, rgba(255,255,255,.012) 0px,
                                        rgba(0,0,0,.012) 2.5px,
                                        rgba(255,255,255,.012) 5px),
      /* 8 the face — beginning where the bevel ends, falling to ink at the far
           corner. --accent is the same value as .ht-fill, so the mass and the
           lit Thread are literally the same material. */
      linear-gradient(135deg, var(--accent-mid) 19.87%,
                              var(--accent)     38%,
                              var(--accent-dk)  68%,
                              var(--ink)       100%);
  }

  /* ---- the catch ---------------------------------------------------------
     THE OBJECT HAS NO LIGHT SOURCE OF ITS OWN. That is the entire concept, and
     it is one animated property: opacity, on one pseudo-element that NEVER
     MOVES. Default 0 = the object AFTER the light has left, which is the
     resting state (and therefore what all four gates resolve to).

     Four guarantees this is not the hero's third travelling light:
       1. nothing translates — a light that does not move cannot travel
       2. the lit region is an EDGE bound to the silhouette, not a band across
          the face; its geometry is static, only its intensity changes
       3. the envelope is ~605ms over a 288px object. A travelling light reads as
          travelling because it is NARROW relative to what it crosses; a level
          that swells and decays on a stationary facet reads as the light in the
          room changing, which is exactly what it is
       4. no head, no core, no ring, no halo, no glow — this is a RESPONSE to the
          one sanctioned light, not a fourth instance of it. Metal still appears
          on exactly three things.
     It must also be fully dark again before 1.75s, when .hj-light begins its
     walk, or the hero has two light sources at once. */
  .settle-face::before {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    background-image: linear-gradient(135deg,
      rgba(255,255,255,.46) 13.82%,
      rgba(255,255,255,.40) 17.00%,
      rgba(255,255,255,.24) 19.87%,
      rgba(255,255,255,0)   24.00%);
    opacity: 0;
  }

  /* ---- the specular ------------------------------------------------------
     THE OBJECT DOES NOT MOVE. THE LIGHT ON IT DOES. No rotate3d, no tilt, no
     parallax on the mass: a heavy object that leans when you wave at it is a
     floating card, which is the one thing the brief names twice. It is also
     just what a real object does — walk past a machined block and the block
     stays put; the highlight on its face walks.

     THE DAMPING IS THIS TRANSITION AND NOTHING ELSE. Retargeting a running
     transition every frame makes the compositor re-interpolate from wherever it
     currently is: an exponential chase, ~0.4s of perceived lag, for free, and
     costing precisely zero when the pointer is still. A lerp would add a FOURTH
     permanent rAF beside Lenis, the cursor and the scroll engine to produce an
     identical curve.

     inset: -10% so the translated layer can never expose an unlit gap; the
     parent's clip-path contains it, so it can never paint outside the mass and
     can never touch a glyph. will-change is permanent HERE and only here: this
     layer is retargeted on arbitrary pointer moves for the life of the page, so
     re-promoting it on every first move would hitch each time. */
  .settle-face::after {
    content: ''; position: absolute; inset: -10%; pointer-events: none;
    background-image: radial-gradient(58% 52% at 26% 18%,
      rgba(255,255,255,.085) 0%,
      rgba(255,255,255,.035) 40%,
      rgba(255,255,255,0)    72%);
    transform: translate3d(var(--settle-px, 0px), var(--settle-py, 0px), 0);
    transition: transform 1.1s var(--e-out);
    will-change: transform;
  }

  /* ---- the arrival -------------------------------------------------------
     THE ONLY THING BEHIND THE JS GATE is the un-arrived state, and the gate
     releases itself inside the same settle() that releases the spine's.
     will-change lives HERE, not on the base rule: the layer exists from first
     paint (so there is no promotion hitch at 0.10s) and is dropped by the same
     attribute that ends the animation — the discipline finish() already applies
     with willChange = 'auto'.

     Two channels, split on purpose: opacity finishes at 0.60s while the rise
     still has 23 of its 28px to go, so what you watch for the last 400ms is a
     SOLID object still moving. That is the whole difference between a heavy
     arrival and a fade-in. */
  .js-enabled .settle:not([data-settled]) {
    opacity: 0;
    transform: translate3d(0, var(--settle-rise), 0);
    will-change: transform, opacity;
  }
  .settle.is-arriving {
    animation: settleRise    var(--settle-dur) var(--e-settle) var(--settle-t0) forwards,
               settleSurface .50s              var(--e-out)    var(--settle-t0) forwards;
  }
  /* `linear` with the shape carried in the keyframes, exactly as htFlash does —
     same idiom, one file. */
  .settle.is-arriving .settle-face::before {
    animation: settleCatch var(--settle-lit-dur) linear var(--settle-lit-t0) forwards;
  }

  @keyframes settleRise    { from { transform: translate3d(0, var(--settle-rise), 0) }
                             to   { transform: translate3d(0, 0, 0) } }
  @keyframes settleSurface { from { opacity: 0 } to { opacity: 1 } }
  @keyframes settleCatch {
    0%   { opacity: 0 }
    22%  { opacity: .34 }
    50%  { opacity: 1 }        /* the light is level with the object's centre */
    74%  { opacity: .46 }
    100% { opacity: 0 }        /* the light has walked away. This is the rest. */
  }

  /* ---- the fail-open gates ----------------------------------------------
     None depends on another, and all four resolve to the SAME state: the block,
     fully formed, facet at its resting value, no catch, no drift.
       1. the CSS default above IS that state — JS blocked, no code runs
       2. data-settled is set on a timer inside the spine's own settle(),
          regardless of what any animation does, plus in initHeroSequence()'s
          catch block, plus a guard if the thread is absent entirely
       3. .js-fallback, after the head script's 3s watchdog
       4. prefers-reduced-motion
     `transition: none` on the specular is not decoration: without it, forcing
     the transform to none while a chase is live would ANIMATE it home over
     1.1s — a fail-open gate that fails open slowly. */
  .js-fallback .settle {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  .js-fallback .settle-face::before { animation: none !important; opacity: 0 !important; }
  .js-fallback .settle-face::after  { transition: none !important; transform: none !important; }

  @media (prefers-reduced-motion: reduce) {
    .settle, .settle.is-arriving {
      animation: none !important; opacity: 1 !important; transform: none !important;
    }
    .settle-face::before,
    .settle.is-arriving .settle-face::before { animation: none !important; opacity: 0 !important; }
    /* also catches a MID-SESSION preference change: the JS listener is gated at
       init like everything else in this file, so these !importants are what let
       a user who flips the OS setting with the tab open get stillness without a
       reload, and without this file growing the only matchMedia change listener
       in the build. */
    .settle-face::after { transition: none !important; transform: none !important; }
  }
}
/* ===== end ===== */

/* ===== v19d — THE TWO-COLUMN HERO ======================================
   60/40. Copy left, the system right. The owner's direction, and a deliberate
   reversal of two things v19a-c held: the hero is no longer monochrome-only
   drawing, and it now carries images again.

   WHAT IS RASTER AND WHAT IS CODE. The five objects are generated raster; every
   line between them is an SVG path taking currentColor from --accent. That
   split is the technique. The model renders objects; the page draws structure.
   The connectors are therefore crisp at every DPR, exactly on-palette, and
   animatable — and the first one starts OUTSIDE the left edge of the viewBox,
   which is the Thread arriving.

   The whole illustration is ONE <svg> with ONE viewBox, items and links
   together. A link endpoint cannot drift off a panel when the column resizes,
   because there is no second coordinate space for it to drift in.

   THE SETTLE (v19c) is gone from this build — two objects in one hero is one
   too many. Its CSS block below is dead in v19d and can be cut wholesale.

   APPEND AT EOF; nothing may follow.
   ====================================================================== */

/* Below 1100 the hero stays exactly as v19b left it: one column, no
   illustration. The illustration needs ~400px of its own to read, and taking
   that from a 1008px content box would set the 84px h1 in four ragged lines —
   the same measured trap that killed v19c's reserved column. */
.hviz { display: none; }

@media (min-width: 1100px) {

  /* ---- the split ---------------------------------------------------------
     Grid, not float and not absolute: the illustration has to be able to set
     its own row height and the trust strip has to span both columns, and grid
     is the only one of the three that does both without a magic number.

     .hero-thread, .hero-journey and .hero-thread-cap are absolutely positioned
     against .container and are NOT affected — an abs-pos child of a grid
     container still resolves against its padding box, so the Thread stays on
     --thread-x and the journey rail still spans the full width. Verified.

     row-gap is stated rather than inherited: grid items do NOT collapse
     margins, so the 32px that used to collapse out of .hero-cta's bottom now
     sits inside .hero-copy. Zeroed below, and the gap declared here instead, or
     the hero silently grows by 32px. */
  .hero > .container {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 40%);
    column-gap: 56px;
    row-gap: 0;
    align-items: start;
  }
  .hero-copy   { grid-column: 1; grid-row: 1; }
  .hero-credit { grid-column: 1; grid-row: 2; }
  .hviz        { grid-column: 2; grid-row: 1 / span 2; }
  /* the plinth still springs from the spine and still runs the full width —
     it is the one horizontal rule in the hero and the split must not break it */
  .hero .trust-strip { grid-column: 1 / -1; grid-row: 3; }

  /* THE HEADLINE. 84px was set for a full-width measure. In a 60/40 split the
     copy column is ~693px at 1600 and ~605px at 1200, and 84px sets a
     53-character headline in four ragged lines there — measured, and the exact
     failure that killed v19c's reserved column. 64px is the largest size that
     holds it to three clean lines at 1100. The WORDS do not change. */
  [data-thread] .hero-title {
    font-size: clamp(40px, 4.4vw, 64px);
    max-width: 20ch;
  }
  /* 52ch, not 46: at 46 the lead set in FOUR lines in the narrowed column and
     pushed the journey rail through the 900px fold. Measured — 52ch holds it to
     three lines at every width from 1100 up. */
  .hero .lead  { max-width: 52ch; margin-bottom: 26px; }
  .hero-cta    { margin-bottom: 0; }      /* the collapse that no longer happens */
  .hero-credit { margin-top: 22px; max-width: 52ch; }
  .hero-meta   { margin-bottom: 24px; }

  /* ---- the illustration --------------------------------------------------
     aspect-ratio matches the viewBox exactly, so the box is reserved at first
     paint and the five images cannot shift anything as they decode. Zero CLS by
     construction, not by tuning. */
  .hviz {
    display: block;
    position: relative;
    align-self: center;
    width: 100%;
    max-width: 480px;
    margin-left: auto;
    color: var(--accent);                 /* the links' currentColor */
  }
  .hviz-net {
    display: block; width: 100%; height: auto;
    aspect-ratio: 480 / 560;
    overflow: visible;                    /* the entry link starts at x = -40 */
  }

  /* ---- the links ---------------------------------------------------------
     THE DEFAULT IS FULLY DRAWN. As everywhere else in this build, the undrawn
     state is the only thing behind the JS gate, so JS blocked means a complete
     illustration with no code having run.

     stroke-dashoffset is used here and was REJECTED for the journey rail in
     v19a, and the difference is real rather than a change of mind: there it was
     one long path repainting every frame of a scroll-coupled animation; here it
     is five short paths, one shot, at load. Total stroked length is ~1,100px
     across five elements and it runs once. */
  .hv-link { stroke-dasharray: var(--hv-len, 400); stroke-dashoffset: 0; }
  .js-enabled .hviz:not([data-linked]) .hv-link { stroke-dashoffset: var(--hv-len, 400); }

  /* ---- the items ---------------------------------------------------------
     transform-box: fill-box so a percentage transform-origin resolves against
     the image's own box rather than the SVG's user space — without it every
     item scales from the top-left corner of the viewBox and they fly apart.
     The rise is 10 user units, which at the 480-unit box is ~1.7% of the width:
     a settle, not an entrance. */
  .hv-item { transform-box: fill-box; transform-origin: 50% 60%; opacity: 1; }
  .js-enabled .hviz:not([data-linked]) .hv-item {
    opacity: 0; transform: translateY(10px);
  }

  /* ---- the sequence ------------------------------------------------------
     Item, then its link, then the next item: the panel exists before the line
     that connects it, because a line cannot arrive at something that is not
     there yet. Starts at 0.55s — after the h1's lines are moving, so the
     illustration is never the first thing to arrive — and ends at 2.68s, inside
     the ~3.1s first-load ceiling this build has held since v19a.

     The entry link (hv-l0) fires FIRST and alone: it is the Thread reaching the
     illustration, so it has to precede everything it connects. */
  .hviz.is-linking .hv-l0 { animation: hvDraw .34s var(--e-out) .50s forwards; }
  .hviz.is-linking .hv-i1 { animation: hvIn   .52s var(--e-out) .70s forwards; }
  .hviz.is-linking .hv-l1 { animation: hvDraw .34s var(--e-out) 1.04s forwards; }
  .hviz.is-linking .hv-i2 { animation: hvIn   .52s var(--e-out) 1.22s forwards; }
  .hviz.is-linking .hv-l2 { animation: hvDraw .40s var(--e-out) 1.56s forwards; }
  .hviz.is-linking .hv-i3 { animation: hvIn   .52s var(--e-out) 1.78s forwards; }
  .hviz.is-linking .hv-l3 { animation: hvDraw .34s var(--e-out) 2.12s forwards; }
  .hviz.is-linking .hv-i4 { animation: hvIn   .52s var(--e-out) 2.30s forwards; }
  .hviz.is-linking .hv-l4 { animation: hvDraw .34s var(--e-out) 2.64s forwards; }
  .hviz.is-linking .hv-i5 { animation: hvIn   .52s var(--e-out) 2.82s forwards; }
  .hviz.is-linking .hv-l5 { animation: hvDraw .26s var(--e-out) 3.20s forwards; }

  @keyframes hvDraw { from { stroke-dashoffset: var(--hv-len, 400) }
                      to   { stroke-dashoffset: 0 } }
  @keyframes hvIn   { from { opacity: 0; transform: translateY(10px) }
                      to   { opacity: 1; transform: translateY(0) } }

  /* ---- the fail-open gates ----------------------------------------------
     All four resolve to the same state: every item opaque and in place, every
     link fully drawn. */
  .js-fallback .hv-link { stroke-dashoffset: 0 !important; animation: none !important; }
  .js-fallback .hv-item { opacity: 1 !important; transform: none !important; animation: none !important; }

  @media (prefers-reduced-motion: reduce) {
    .hv-link, .hviz.is-linking .hv-link {
      stroke-dashoffset: 0 !important; animation: none !important;
    }
    .hv-item, .hviz.is-linking .hv-item {
      opacity: 1 !important; transform: none !important; animation: none !important;
    }
  }
}

/* THE SETTLE is not in v19d's markup. Its rules are inert; the block is left in
   place only so v19c and v19d stay diffable. Delete on the next pass. */
.settle { display: none !important; }
/* ===== end ===== */

/* ===== v19e — BLACK FOR ACTION, ONE COLOUR FOR ALIVENESS ================
   The question was "keep grey or add blue". Both answers were wrong on their
   own: the all-blue comp put the accent in nine places — nav button, a word
   inside the h1, the primary CTA, tick icons, four eyebrows, four icon tiles,
   four numbered circles, the connectors and the CTA band — and once everything
   is accented nothing is emphasised. The all-grey comp looked better and more
   ownable, but gave the eye nothing to follow on the first screen.

   So the accent is not a brand field here, it is a SIGNAL, and it obeys one
   rule that can be checked in a screenshot:

       BLACK  = you can act on this      (buttons, and only buttons)
       LIVE   = this is alive or moving  (~2% of pixels, listed below)
       GREY   = everything else

   The complete list of things allowed to be live. If you add a sixth, the rule
   is broken and the accent is decoration again:
     1. the status pill's dot            — "accepting new clients" is a live fact
     2. the Thread's travelling light    — the page's one moving object
     3. the journey walk's light         — the same light, handed over
     4. the hero node, once the light has passed it
     5. the active stage marker in #system
     6. a connector in the hero illustration WHILE IT DRAWS — and it settles
        back to grey the moment it is finished, because a connection that has
        been made is infrastructure, not news

   Coverage is deliberately tiny: at rest, after every animation has finished,
   the only live pixels on the first screen are one 8px dot and whatever the
   scroll is currently lighting.

   APPEND AT EOF; nothing may follow.
   ====================================================================== */

:root {
  /* Generic on purpose. At this coverage a signal colour does not carry brand —
     the silver does — so the only jobs left are "reads as live" and "legible at
     1.5px". A distinctive hue would fail the second and buy nothing on the
     first. 5.17:1 on --bg. */
  --live:     #2563eb;
  --live-dk:  #1d4ed8;
  --live-a12: rgba(37, 99, 235, .12);
  --live-a20: rgba(37, 99, 235, .20);
  --live-a55: rgba(37, 99, 235, .55);
  --live-a28: rgba(37, 99, 235, .28);

  /* The ink face for the primary button. Same 135deg light direction as --grad,
     so the two buttons are lit from the same place even though one is metal and
     one is ink. */
  --ink-grad: linear-gradient(135deg, #2b3039 0%, #17191f 52%, #0d0f13 100%);
}

/* ---------- 1-5. the lit markers ----------------------------------------
   The marker family already carries "lit" as one property on one pseudo —
   opacity on ::after — so making lit mean LIVE is a re-point of that pseudo's
   ring and halo, and nothing else. The metal face underneath is untouched: the
   marker still reads as a machined part, now with a live ring.

   .ts-dot is DELIBERATELY EXCLUDED. The trust strip's four dots are labels, not
   state — nothing about them is alive — and they are also the largest group of
   markers on the first screen. Letting them go live would take the accent from
   ~2% of pixels to something the eye reads as a colour scheme. */
.status-pill .dot::after,
.hero-node::after,
.sn-dot::after,
.badge-core::after {
  box-shadow: inset 0 0 0 var(--dot-ring, 2px) var(--live),
              0 0 0 3px var(--live-a12);
}

/* the active stage marker — the one thing in #system that is "now" */
.sys-node.is-active .sn-dot { --dot-fill: var(--live); --dot-line: var(--live); }

/* ---------- 2-3. the travelling light ------------------------------------
   The core stays metal — it is still the same object it has been since v19 —
   and only the ring and the halo go live, so the light reads as a lit metal
   head rather than as a blue dot. Both the spine's head and the journey walk's
   head are the same recipe and are re-pointed together, exactly as the shared
   selector list intends. */
.js-enabled .ht-light,
.js-enabled .hj-light {
  box-shadow: var(--metal-inset),
              0 0 0 1.5px var(--live),
              0 0 12px 3px var(--live-a55),
              0 0 26px 6px var(--live-a28);
}
@media (max-width: 900px) {
  /* the halo is trimmed here because the light passes within 19px of the copy;
     the live version keeps that trim */
  .js-enabled .ht-light,
  .js-enabled .hj-light {
    box-shadow: var(--metal-inset),
                0 0 0 1.5px var(--live),
                0 0 10px 2px var(--live-a55);
  }
}

/* ---------- 6. the connectors, live only while they draw -----------------
   The default — JS off, reduced motion, and every state after the sequence
   finishes — is GREY. The accent exists for the ~2.9s in which the system is
   being connected in front of you, and then it goes away. That is the whole
   argument for the colour being a signal: it marks the moment, not the object.

   `stroke` is not a compositor-friendly property, which is why this is a
   one-shot transition on six short paths at load and never a scroll-coupled
   one. */
@media (min-width: 1100px) {
  .hviz.is-linking .hv-link { stroke: var(--live); }
  .hviz[data-linked] .hv-link {
    stroke: var(--accent);
    transition: stroke 1.2s var(--e-out);
  }
}

/* ---------- BLACK FOR ACTION --------------------------------------------
   INDEPENDENTLY REVERTIBLE: delete this sub-block and the primary button goes
   back to machined silver with nothing else affected.

   Why it changes at all: the machined face is a LIGHT object on a light page.
   Measured on v19c, its darkest pixel is L=0.611 against a page at L=0.965 —
   it is legible and expensive-looking, and it is carrying the single most
   important click on the site with very little tonal separation. Ink gives it
   the highest contrast available without spending the accent on it, which is
   what keeps rule one ("black for action") from collapsing into rule two.

   White on #17191f is 15.9:1. */
.btn-primary { color: #fff; }
.btn-primary::before {
  background-image: var(--ink-grad);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12),
              inset 0 0 0 1px rgba(255, 255, 255, .05);
}
.btn-primary::after {
  background-image: linear-gradient(135deg, rgba(255,255,255,.16) 0%, rgba(255,255,255,0) 58%);
}

/* THE DARK SECTIONS KEEP THE METAL BUTTON. #system, the CTA band and the footer
   are ink grounds — an ink button on an ink band is an invisible button, which
   is a conversion bug, not a style choice. Metal is the correct face there and
   always was. */
.cta-band .btn-primary,
.footer .btn-primary,
#system .btn-primary { color: var(--on-metal); }
.cta-band .btn-primary::before,
.footer .btn-primary::before,
#system .btn-primary::before {
  background-image: var(--grad);
  box-shadow: var(--metal-inset);
}
.cta-band .btn-primary::after,
.footer .btn-primary::after,
#system .btn-primary::after { background-image: var(--metal-sheen); }

/* ...EXCEPT the form, which is a WHITE panel nested inside the ink band. The
   rule above is about the GROUND the button sits on, not the section it lives
   in, and .qform's ground is white — so the site's single most important submit
   would otherwise have been the one button left in silver. The build already
   makes exactly this distinction one line at a time for --focus
   (`.qform, .form-done { --focus: var(--accent) }`); this is the same carve-out
   for the same reason, and inheritance handles the nesting. */
.qform .btn-primary,
.form-done .btn-primary { color: #fff; }
.qform .btn-primary::before,
.form-done .btn-primary::before {
  background-image: var(--ink-grad);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12),
              inset 0 0 0 1px rgba(255, 255, 255, .05);
}
.qform .btn-primary::after,
.form-done .btn-primary::after {
  background-image: linear-gradient(135deg, rgba(255,255,255,.16) 0%, rgba(255,255,255,0) 58%);
}
/* ===== end ===== */

/* ===== v20 — the owner's five objects, no connectors ====================
   Same hero as v19e. Three changes and nothing else:
     1. the five images are crops of the owner's own frame, not generated art
     2. the connectors are removed — the composition is five objects in one
        space, not a wired diagram
     3. the box is 480x600 rather than 480x560, because the five cropped objects
        need the extra 40 units to stagger without touching
   The .hv-link rules above are now inert. They are deliberately not deleted:
   nothing selects them, and leaving them keeps v19e and v20 diffable.
   ====================================================================== */
@media (min-width: 1100px) {
  .hviz-net { aspect-ratio: 480 / 600; }
  /* each object keeps its own contact shadow, which the crop dropped along with
     the source's detached shadow ellipse. Reinstated here so it can be tuned per
     surface instead of being baked at one lightness for one background. */
  .hv-item { filter: drop-shadow(0 10px 16px rgba(20, 22, 26, .16)); }
}

/* ===== v20b — the floated cluster, hover, and the nav gap ===============
   The five objects are the owner's own transparent exports, trimmed to their
   silhouettes (docs/prep-hero-items.mjs). Nothing generated, no connectors.
   ====================================================================== */
@media (min-width: 1100px) {

  /* ---- hover -------------------------------------------------------------
     Lift, a little scale, and a deeper cast. --e-back is this build's own
     overshoot curve and is used by every other hover on the site, so these read
     as the same gesture as a card lift rather than a new one.

     It works only because .is-linking is REMOVED when the entrance finishes:
     an `animation … forwards` fill outranks a :hover rule, so if the class
     stayed the object would be frozen at the animation's end transform and the
     hover would silently do nothing. initHeroViz's done() already drops it.

     transform and filter only. drop-shadow is a filter, so it is the one
     property here that can cost a repaint — it is on a small element, changing
     on a discrete state, never on scroll. */
  .hv-item {
    pointer-events: auto;
    transition: transform .55s var(--e-back), filter .45s var(--e-out);
  }
  .hv-item:hover {
    transform: translateY(-12px) scale(1.05);
    filter: drop-shadow(0 24px 34px rgba(20, 22, 26, .26));
  }
  /* THE NEIGHBOURS ARE LEFT ALONE. There used to be a rule receding every other
     object to scale(.985) while one was hovered — "depth by contrast". It reads
     fine on a static cluster and badly on a floating one: each object carries
     its own drift on its own period, and pushing a transform onto all five at
     once made the whole group twitch whenever the pointer entered any one of
     them. Five independent objects should behave independently, so hover now
     touches only the object under the pointer. */

  @media (prefers-reduced-motion: reduce) {
    .hv-item, .hv-item:hover, .hviz:hover .hv-item:not(:hover) {
      transition: none !important; transform: none !important;
    }
  }
}

/* ---- the gap under the nav ----------------------------------------------
   The header floats over the page with no ground of its own, so at the top of
   the scroll the 66px shell sat hard against the hero's first row and the nav's
   own text read as top-aligned to the page rather than centred in its band.
   +20px above the hero gives the shell air on both sides. .nav-shell's height
   and padding are untouched — its label was already centred inside itself; what
   was missing was room around it. */
.hero { padding-top: 116px; }

/* ===== v20c — captions, connectors, and the nav's floor =================
   Additive only. Nothing is removed: the Thread, the journey rail, the colour
   rule and the ink CTA are all exactly as v19e left them.
   ====================================================================== */

/* ---- the nav's floor ----------------------------------------------------
   A hairline the full width of the viewport, under the header, ONLY while the
   page is at the top. It gives the nav a ground so its label reads as centred
   in a band rather than floating on the hero; the moment the shell picks up its
   own background and shadow (.is-stuck) the line has nothing left to do and
   would be a second edge under a rounded object, so it fades out.

   On .nav, not on .nav-shell: the shell is a rounded pill, and a border on it
   would follow that radius and read as an underline on a button. This is the
   page's edge, so it spans the page. */
.nav::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  height: 1px; background: var(--hairline);
  opacity: 1; transition: opacity .35s var(--e-out);
  pointer-events: none;
}
.nav.is-stuck::after { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .nav::after { transition: none; } }

@media (min-width: 1100px) {
  /* ---- the captions ------------------------------------------------------
     SVG <text> rather than HTML positioned over the box, so the caption shares
     the objects' coordinate space and cannot drift from its panel at any width.
     Sizes are in viewBox units — at 1600 the box renders ~1:1, and at 1100 it
     scales to ~0.82, which is why the name is set at 15 rather than 13. */
  .hv-cap {
    font-family: inherit;
    font-size: 15px; font-weight: 600; letter-spacing: -.01em;
    fill: var(--text);
  }
  .hv-sub {
    font-size: 12.5px; font-weight: 400; letter-spacing: 0;
    fill: var(--muted);                 /* 8.0:1 on --bg */
  }
  /* the captions are content, so they never animate and never rest below 1 */
  .hv-caps { opacity: 1; }

  /* ---- the connectors ----------------------------------------------------
     Hairline, not --accent: these join five objects that already carry their own
     weight, and at --accent they competed with the Thread for the eye. They are
     also NOT part of the live-colour rule — a connector here is structure, not
     state. */
  .hv-link { stroke-dasharray: none; stroke-dashoffset: 0; }
  .js-enabled .hviz:not([data-linked]) .hv-link { stroke-dashoffset: 0; }

  /* ---- the cluster's own hover, retuned for the smaller objects ---------- */
  .hv-item:hover { transform: translateY(-9px) scale(1.06); }
}


/* ===== v20d — the Thread goes straight down, and each stage arrives whole ===
   Two changes, both from the owner's markup on the v20 screenshot.
   ====================================================================== */

/* ---- 1. DETACH THE RAIL --------------------------------------------------
   The journey rail left the spine, ran the width of the hero, dropped at the far
   right and came back — and carried ATTRACT / CONVERT / FOLLOW UP / MEASURE /
   IMPROVE across the hero's foot. Both are gone: the horizontal legs and the
   five words with them.

   The rail is hidden rather than deleted from the markup, so nothing else in the
   hero shifts and v19e stays one CSS block away. Its three consequences are
   undone here: the spine runs to the hero's bottom edge again instead of
   stopping 72px short at a rail that is no longer drawn, the container stops
   reserving the rail's 102px band, and the caption returns to the flow.

   THE THREAD ITSELF IS UNTOUCHED — same x, same width, same strike, same light.
   It simply no longer turns right. */
.hero-journey { display: none !important; }
.hero > .container { padding-bottom: 0; }
.hero-thread { bottom: calc(var(--sec-gap) * -1); }
.hero-thread-cap {
  position: static;
  margin: 30px 0 0 var(--ht-offset-c);
  padding-left: 0;
}

/* ---- 2. ONE STAGE, ONE ARRIVAL ------------------------------------------
   Previously the five objects animated on one clock, the captions never
   animated at all, and the connectors were static. A stage is one idea, so its
   object, its words and the line that reaches it now share a single delay and
   arrive as one thing.

   The link that arrives with stage N is the one that CONNECTS INTO N, which is
   why hv-l1 rides with stage 2 and there is no link on stage 1 — nothing
   connects into the first stage.

   Paint order is unchanged: the links stay in their own <g> ahead of the images
   so a connector always passes behind a panel. Only their timing moved. */
@media (min-width: 1100px) {
  .hv-cap { opacity: 1; }
  .js-enabled .hviz:not([data-linked]) .hv-cap,
  .js-enabled .hviz:not([data-linked]) .hv-link { opacity: 0; }

  .hviz.is-linking .hv-i1, .hviz.is-linking .hv-c1 { animation: hvIn .55s var(--e-out) .70s forwards; }
  .hviz.is-linking .hv-l1,
  .hviz.is-linking .hv-i2, .hviz.is-linking .hv-c2 { animation: hvIn .55s var(--e-out) 1.06s forwards; }
  .hviz.is-linking .hv-l2,
  .hviz.is-linking .hv-i3, .hviz.is-linking .hv-c3 { animation: hvIn .55s var(--e-out) 1.42s forwards; }
  .hviz.is-linking .hv-l3,
  .hviz.is-linking .hv-i4, .hviz.is-linking .hv-c4 { animation: hvIn .55s var(--e-out) 1.78s forwards; }
  .hviz.is-linking .hv-l4,
  .hviz.is-linking .hv-i5, .hviz.is-linking .hv-c5 { animation: hvIn .55s var(--e-out) 2.14s forwards; }

  /* the captions must not carry the objects' translate — a word that slides is a
     word you can catch mid-move, and every string in this build resolves to
     opacity 1 in place. They fade only. */
  .hviz.is-linking .hv-cap { animation-name: hvFade; }
  @keyframes hvFade { from { opacity: 0 } to { opacity: 1 } }

  /* the four fail-open gates, unchanged in shape: all resolve to everything
     present, in place, at full opacity */
  .js-fallback .hv-cap, .js-fallback .hv-link { opacity: 1 !important; animation: none !important; }
  @media (prefers-reduced-motion: reduce) {
    .hv-cap, .hv-link,
    .hviz.is-linking .hv-cap, .hviz.is-linking .hv-link { opacity: 1 !important; animation: none !important; }
  }
}

/* ===== v20e — hero centred, plinth removed, right column widened =========
   Owner's markup on the v20 screenshot: delete the trust strip and the caption
   beneath it, centre the hero vertically, give the illustration more room.
   ====================================================================== */

/* ---- 1. the plinth goes ---------------------------------------------------
   The four trust pills and `Visitors -> Customer` are hidden, not deleted from
   src/pages/index.html, so this is one rule to undo. The strip's border-top was
   the hero's only horizontal rule and the thing the Thread's plinth sprang
   from; with it gone the hero has no internal edges at all, which is why the
   vertical centring below is now possible without the layout looking bottom-
   heavy. */
.hero .trust-strip,
.hero-thread-cap { display: none; }

/* ---- 2. vertically centred ------------------------------------------------
   min-height on the SECTION and centring on its container, rather than padding
   tuned per breakpoint: the hero then holds the middle of the viewport at any
   height, including the short-and-wide laptop that padding maths always misses.

   .hero-bg and .gem are absolutely positioned, so they are not flex items and
   nothing about the background moves. .hero-thread is absolute against
   .container and follows it exactly as before — the Thread is untouched. */
.hero { padding-top: 116px; }

@media (min-width: 1100px) {
  /* Centring is desktop-only. Below 1100 the hero is taller than the viewport
     anyway, so min-height would never bind and the shorter bottom padding would
     only tighten the seam to the next section. Mobile keeps v19e's rhythm. */
  .hero {
    display: flex; align-items: center;
    min-height: 100vh;
    /* ASYMMETRIC ON PURPOSE. The nav is fixed and covers the top ~90px, so
       padding split evenly centres the block in the viewport and lands it
       visually low — the eye measures from the nav's underside, not from y=0.
       116 above and 26 below puts the free space either side of the content
       equal *below the nav*: measured at 1600x900, 44px clear under the nav and
       44px clear at the hero's foot. */
    padding-top: 116px;
    padding-bottom: 26px;
  }
  .hero > .container { width: 100%; }
}

@media (min-width: 1100px) {
  /* ---- 3. more room on the right ----------------------------------------
     40% -> 44% and the gutter 56 -> 40. Measured at 1600: the illustration's
     column goes 499 -> 549 and the copy 693 -> 659, which is still wide enough
     to hold the 64px headline at three lines. The box's own max-width rises
     with it so the extra width reaches the objects rather than becoming margin. */
  .hero > .container { grid-template-columns: minmax(0, 1fr) minmax(0, 44%); column-gap: 40px; }
  .hviz { max-width: 528px; }
}

/* ===== v20f-a — THE CONNECTORS DRAW =====================================
   Owner: "the line on the right should also animate and move".

   They were fading in with their stage, which is why they read as static
   furniture that happened to appear: a fade tells you a line EXISTS, a draw
   tells you a connection is being MADE. Overrides the opacity treatment in the
   v20d block for .hv-link only — the captions and objects there are unchanged.

   stroke-dashoffset is used deliberately and this is not a reversal of the v19a
   decision that rejected it for the journey rail. That was ONE long path
   repainting every frame of a scroll-coupled animation. These are four short
   paths, one shot, at load. Different problem.

   --hv-len is written per path by initHeroViz() from getTotalLength(), once at
   init and never again — the path data is in viewBox units and does not change
   with the box. The CSS fallback (400) is longer than the longest path in the
   set, so every connector is fully hidden before JS runs and there is no flash
   if it never does.

   TIMING: each line starts 200ms BEFORE its destination panel and finishes
   140ms after it. The line reaches out toward where the panel will be, the panel
   lands mid-draw, the line completes onto it. That ordering is the whole
   difference between a system assembling and a slideshow advancing — a line
   that only appears once both ends exist has nothing to connect.
   ====================================================================== */
@media (min-width: 1100px) {
  .hv-link {
    stroke-dasharray: var(--hv-len, 400);
    stroke-dashoffset: 0;
    opacity: 1;                                   /* the fade is retired */
  }
  .js-enabled .hviz:not([data-linked]) .hv-link {
    stroke-dashoffset: var(--hv-len, 400);
    opacity: 1;
  }

  /* the stage delays are 0.70 / 1.06 / 1.42 / 1.78 / 2.14; each link sits 200ms
     ahead of the panel it reaches */
  .hviz.is-linking .hv-l1 { animation: hvDraw .34s var(--e-out) .86s forwards; }
  .hviz.is-linking .hv-l2 { animation: hvDraw .34s var(--e-out) 1.22s forwards; }
  .hviz.is-linking .hv-l3 { animation: hvDraw .34s var(--e-out) 1.58s forwards; }
  .hviz.is-linking .hv-l4 { animation: hvDraw .34s var(--e-out) 1.94s forwards; }

  .js-fallback .hv-link {
    stroke-dashoffset: 0 !important; opacity: 1 !important; animation: none !important;
  }
  @media (prefers-reduced-motion: reduce) {
    .hv-link, .hviz.is-linking .hv-link {
      stroke-dashoffset: 0 !important; opacity: 1 !important; animation: none !important;
    }
  }
}

/* ===== v20f — THE FIRST SCREEN: TYPE, RHYTHM, AND A HAND THAT DRAWS =======
   A hero pass from a hero-section specialist. Nothing below the fold is
   touched, and every number below was taken off the rendered page rather than
   chosen.

   WHAT IT OVERRIDES, so the diff is readable:
     v20b/v20e  .hero padding 116/26          -> named nav band + air
     v19d       .hero-title clamp / max-width -> derived from the 4-line ceiling
     v19d       .hero .lead max-width 52ch    -> the column is the measure
     v19d/v20e  .hviz row participation       -> the copy defines the height
     v20c       .hv-cap / .hv-sub sizes       -> two bands, constant rendered px
     v20d       .hv-l* fade                   -> superseded by v20f-a, then this
     v20f-a     .hv-l* draw timing/easing     -> linear, one velocity, per-link cool
   APPEND AT EOF; nothing may follow.
   ====================================================================== */

/* ---- 1. THE FRAME -------------------------------------------------------
   The 116/26 asymmetry was RIGHT and is kept as an idea: the nav is fixed and
   covers the top of the hero, so the eye measures free space from the shell's
   underside, not from y=0. Split the padding evenly and the block lands
   visually low.

   What it lacked was a FLOOR. Measured at 1600x768 the clear space under the
   nav collapses to 30px, because 116 minus the nav's 86 is all that is left
   once min-height stops binding. Naming the two quantities makes the floor
   explicit and makes the result symmetric BY CONSTRUCTION rather than by
   landing 4px off: top clear = air, bottom clear = air, at every height.

   --hero-nav is the ONE number here that must be re-measured if the nav shell
   changes height. It is named so that it will be. */
@media (min-width: 1100px) {
  .hero {
    --hero-nav: 86px;              /* nav-shell top 20 + height 66 = underside */
    --hero-air: 64px;              /* 8u — the minimum breath under the nav    */
    padding-top:    calc(var(--hero-nav) + var(--hero-air));
    padding-bottom: var(--hero-air);
  }
}

/* ---- 2. THE COPY DEFINES THE HERO'S HEIGHT ------------------------------
   .hviz spans two grid rows, so whenever the illustration is taller than
   copy + credit the excess is distributed into the spanned tracks. Measured:
   the AUTHORED 22px gap between the CTA and the review renders as 47.6px at
   1600 and 48.6px at 1440, and correctly as 22px at 1280 and 1100 where the
   illustration happens to be shorter. A rhythm value that changes by 26px
   depending on an image's aspect ratio is not a rhythm value.

   An absolutely positioned child contributes nothing to intrinsic track
   sizing, so taking the <svg> out of flow takes the illustration out of row
   sizing entirely. The COPY then defines the hero's height, which is what
   should define it. preserveAspectRatio="xMidYMid meet" is already on the
   element, so the drawing fits itself into whatever box the copy leaves.

   Zero CLS is not lost, it is improved: the box is now sized by text instead
   of reserved for images, so there is nothing left to reserve. Verified —
   the copy's bottom edge and the illustration's are the same pixel at 1600,
   1440, 1280, 1100, 1600x768 and 1920x1080. */
@media (min-width: 1100px) {
  .hviz     { align-self: stretch; }
  .hviz-net { position: absolute; inset: 0; width: 100%; height: 100%;
              aspect-ratio: auto; }
}

/* ---- 3. THE HEADLINE ----------------------------------------------------
   Sized from the FOUR-LINE CEILING, measured at nine column widths. The
   ceiling is close to linear in column width — the first size that breaks to
   four lines is 0.1147-0.1169 x the column at every width tested — so the
   size is set to 8% below it and the headline's SHAPE stops changing.

   Two expressions of one line, because --pad steps 56 -> 96 at
   `max-width: 1180px` and the copy column therefore falls from 558.1px at
   vw 1180 to 513.9px at vw 1181: a 44.2px cliff across one pixel of viewport.
   No single vw clamp can be safe on both sides of it.

   max-width goes because 20ch resolved to 736px and never bound — the column
   has always been the measure, and pretending otherwise hid the real number.

   Verified 1100 -> 1920: THREE lines at every width, widest line 93.6-96.0%
   of the column (was 98.8% at 1280 — 3.5px from the four-line break that
   HANDOFF.md calls the failure that killed v19c), measure locked at
   16.9-17.4ch. Bigger everywhere: +3.1% at 1600, +8.9% at 1100.

   1.02 -> 1.06 leading: at 64/1.02 the descender of "your" on line one closes
   on the cap of "reliable" on line two. Still display leading; the non-hero h1
   uses 1.04.

   text-wrap: pretty is INSURANCE ONLY — measured, it changes nothing on this
   string at any width. DO NOT use `balance`: it produces 499/578/389 at 1600,
   short-over-long, and this build's rag rule is long-over-short. */
@media (min-width: 1100px) {
  [data-thread] .hero-title {
    font-size: clamp(52px, calc(5.746vw - 15.13px), 66px);
    line-height: 1.06;
    letter-spacing: -.032em;
    max-width: none;
    text-wrap: pretty;
    margin: 0 0 24px;               /* 3u — the tightest gap in the column   */
  }
}
@media (min-width: 1100px) and (max-width: 1180px) {
  /* the --pad 56 band: same target, different intercept */
  [data-thread] .hero-title { font-size: clamp(52px, calc(5.746vw - 10.54px), 66px); }
}

/* ---- 4. THE LEAD, AND THE RHYTHM ----------------------------------------
   THE WIDOW. The lead set in four lines at EVERY desktop width and ended on a
   16.3-17.5% last line at every one of them — at 1600 that last line is the
   single word "customers.", sitting directly under an h1 whose own last line
   is the single word "customers."

   20.5px + text-wrap: pretty is the ONLY combination out of four sizes x two
   wrap modes x ten widths that is widow-free everywhere: shortest last line
   29% (was 16.3%), and the fourth line disappears entirely at 1400 and above.
   No FLUID size can do it — last-line fill is a sawtooth in column width, so
   a smooth curve lands in a bad tooth somewhere.

   It reads as a reduction at 1600 (22 -> 20.5) and is a rise everywhere else
   (18.45 -> 20.5 at 1100, +11%). The h1:lead ratio goes 2.91 -> 3.22: the
   hierarchy gets STRONGER, which is the only honest way to answer "make the
   type bigger" when the headline is already at its column's ceiling.

   THE GAPS. Base unit 8px — named because the page is already on it (--pad
   96/56/24, --sec-gap 72). The four gaps were 24/26/26/22: a constant, not a
   rhythm, so nothing grouped. Now 4u / 3u / 5u / 4u, which says: the label
   pair is its own register (32); the headline and its lead are ONE thought so
   their gap is the tightest in the column (24); the seam between reading and
   acting is the widest and the only place the eye should stop (40); the review
   supports the CTA so it stays nearer the button than the button is to the
   lead (32). */
@media (min-width: 1100px) {
  .hero-meta   { margin-bottom: 32px; }                    /* 4u */
  .hero .lead  {
    font-size: 20.5px;                                     /* scale step 2   */
    line-height: 1.5;
    max-width: none;                                       /* 52ch never bound below 1440 */
    text-wrap: pretty;
    margin-bottom: 40px;                                   /* 5u */
  }
  /* the review. text-indent hangs the opening quote: its measured advance is
     6.34px at 18px (~5.81 at 16.5), so -.26em hangs 74% of it and puts the
     quote's optical mass on the same 176 as the h1's T and the lead's W.
     A full hang overshoots — punctuation is lighter than a letterform, not
     absent. .hc-src resets it because a cap line has nothing to hang. */
  .hero-credit { margin-top: 32px; max-width: 52ch; text-indent: -.26em; }  /* 4u */
  .hc-q        { font-size: 16.5px; line-height: 1.5; }    /* step 1, was 15 */
  .hc-src      { font-size: 13px; letter-spacing: .085em;  /* step 0, was 11 */
                 margin-top: 8px; text-indent: 0; }        /* 1u */
}

/* ---- 5. THE CAPTIONS ----------------------------------------------------
   These are set in viewBox units, so their RENDERED size is whatever the
   column happens to be: measured 16.50px at 1600 and 13.59px at 1100 for the
   name, and 13.75 -> 11.33px for the sub. The same words, a 21% swing, driven
   by nothing but window width — and an 11.3px floor on a two-line explanatory
   phrase, which is the real reason the right column reads as an afterthought.
   It is not that the caption is small against a 66px headline (that ratio is
   3.86, which is normal); it is that it is small in absolute terms at the
   narrow end and never the same size twice.

   Two bands, split where the render scale crosses 1.0. Verified rendered:
   name 15.9-17.6px, sub 12.7-14.0px across 1100-1920 — the swing halves and
   the floor rises 1.35px. Rule 2 above does half this work for free: taking
   the SVG out of flow pulled the top scale from 1.100 to 1.029. */
@media (min-width: 1100px) {
  .hv-cap { font-size: 17.4px; letter-spacing: -.008em; }
  .hv-sub { font-size: 13.9px; }
}
@media (min-width: 1276px) {
  .hv-cap { font-size: 16.6px; }
  .hv-sub { font-size: 13.2px; }
}

/* ---- 6. THE EYEBROW GETS A MEASURE --------------------------------------
   .hero-meta is nowrap above 900px and the pill is a fixed 292.6px, so a
   96-character string is set into whatever is left: 346.3px (37.8ch) at 1600,
   256.7px (28.0ch) at 1280 and 200.7px — TWENTY-ONE CHARACTERS — at 1100,
   where it sets in four ragged lines beside a 44px pill.

   Stacking at 1300 rather than 900 costs ~8px of column height at 1100 and
   takes the measure from 21.9ch to 56ch. Measure is the point; height is not.
   Same structure the 900px rule already declares, at a width chosen from a
   measurement rather than inherited from the burger. */
@media (min-width: 1100px) and (max-width: 1300px) {
  .hero-meta { display: block; }
  .hero-meta .status-pill { margin-bottom: 12px; }
}

/* ---- 7. THE CONNECTORS MOVE ---------------------------------------------
   v20f-a made them DRAW, which was right, and they still did not read as
   moving. Measured on the live page: hv-l1 is 50% drawn 46ms into its 340ms
   animation and 98% drawn at 201ms. --e-out is cubic-bezier(.22,1,.36,1) — an
   expo-out — so 86% of the visible motion happens in the first 20% of the
   duration. The line pops, then creeps invisibly for 140ms. That is a fade
   with extra steps, and it is exactly what the owner is objecting to.

   The Thread does not do this and its own comment says why: "CONSTANT
   VELOCITY, one pass, then gone." It is linear. These are the same gesture at
   a smaller scale, so they are linear too.

   ONE HAND, ONE SPEED. All four ran 340ms over lengths of 95.2/73.2/77.3/72.4
   rendered px — 280/215/227/213 px/s, a 31% spread. The Thread runs 748px in
   1400ms = 534 px/s = 519 user-units/s at the new render scale. Every link now
   takes that velocity, so its DURATION FALLS OUT OF ITS OWN MEASURED LENGTH
   rather than being chosen: 86.5u -> 170ms, 66.5u -> 130, 70.3u -> 140,
   65.8u -> 130. The pacing of the whole sequence is measured off the drawing.

   HOW THE DRAW RELATES TO ITS TWO PANELS — the one rule:
       THE LINE LANDS ON THE EXACT FRAME ITS DESTINATION PANEL BEGINS.
   Departure is then a consequence of length, so the line always leaves panel N
   while N is still settling. A line that appears only once both ends exist is
   annotation — nothing is being connected, and that is the slideshow reading
   v20d shipped. A line that completes and then waits has a dead beat in it
   (v20f-a's fixed 340ms leaves 410ms at the tail in which one object fades and
   nothing else happens). A line that reaches into empty space and is answered
   reads as causal: this is a system assembling.

   WHERE IT STARTS is not a taste number. The strike is linear from 350ms over
   1400ms across 748px; object 1's vertical centre sits 82px below the
   container top, so the light draws level with it at
   350 + 1400 x (82/748) = 517ms. Stage 1 fires at 520.
   WHERE IT ENDS is the same argument: stage 5 fires at 1760 and the strike
   completes at 1750. The last object begins arriving in the same frame the
   Thread finishes. The two are on one clock at one speed.

   Sequence ends 2160ms, 530ms earlier than the 2690 it replaces, and nothing
   on the first screen is still moving at 2.2s.

   stroke-dashoffset stands: four short paths, one shot, at load. That is the
   case v19a's rejection carved out and it is not being reopened. */
@media (min-width: 1100px) {

  /* object + caption + the connector INTO it share one stage. Object rises
     and fades on --e-out (a settling object decelerates); the caption fades
     only, because a word that slides is a word you can catch mid-move. */
  .hviz.is-linking .hv-i1, .hviz.is-linking .hv-c1 { animation: hvIn .40s var(--e-out)  .52s forwards; }
  .hviz.is-linking .hv-i2, .hviz.is-linking .hv-c2 { animation: hvIn .40s var(--e-out)  .83s forwards; }
  .hviz.is-linking .hv-i3, .hviz.is-linking .hv-c3 { animation: hvIn .40s var(--e-out) 1.14s forwards; }
  .hviz.is-linking .hv-i4, .hviz.is-linking .hv-c4 { animation: hvIn .40s var(--e-out) 1.45s forwards; }
  .hviz.is-linking .hv-i5, .hviz.is-linking .hv-c5 { animation: hvIn .40s var(--e-out) 1.76s forwards; }
  .hviz.is-linking .hv-cap { animation-name: hvFade; }

  /* the un-drawn state, and only it, lives behind the JS gate. It is also
     where the accent is applied — a fully-offset path shows nothing, so this
     costs no visible blue and means each link is live from the instant it
     starts rather than from the instant the sequence does. */
  .hv-link { stroke-dasharray: var(--hv-len, 400); stroke-dashoffset: 0;
             opacity: 1; stroke: var(--accent); }
  .js-enabled .hviz:not([data-linked]) .hv-link {
    stroke-dashoffset: var(--hv-len, 400); opacity: 1; stroke: var(--live);
  }

  /* draw linear at the Thread's velocity, then cool to grey over 280ms
     starting the moment the draw ends. HANDOFF.md's sixth live thing is "a
     connector WHILE IT DRAWS — and it settles back to grey the moment it
     finishes"; the shipping build holds hv-l1 blue for 1.95s after it
     finished, which is the rule stated and not kept. */
  .hviz.is-linking .hv-l1 { animation: hvDraw .17s linear  .66s forwards,
                                       hvCool .28s linear  .83s forwards; }
  .hviz.is-linking .hv-l2 { animation: hvDraw .13s linear 1.01s forwards,
                                       hvCool .28s linear 1.14s forwards; }
  .hviz.is-linking .hv-l3 { animation: hvDraw .14s linear 1.31s forwards,
                                       hvCool .28s linear 1.45s forwards; }
  .hviz.is-linking .hv-l4 { animation: hvDraw .13s linear 1.63s forwards,
                                       hvCool .28s linear 1.76s forwards; }
  @keyframes hvCool { from { stroke: var(--live) } to { stroke: var(--accent) } }

  /* the v19e block transitions stroke over 1.2s on [data-linked]; each link
     has already cooled on its own clock by then, so the transition would be a
     second, slower cool arriving on top of a finished one. */
  .hviz[data-linked] .hv-link { stroke: var(--accent); transition: none; }

  /* ---- the four gates, all resolving to the SAME state: drawn, grey,
     opaque, untransformed. Nothing here depends on anything above it. */
  .js-fallback .hv-link {
    stroke-dashoffset: 0 !important; stroke: var(--accent) !important;
    opacity: 1 !important; animation: none !important;
  }
  .js-fallback .hv-cap, .js-fallback .hv-item {
    opacity: 1 !important; transform: none !important; animation: none !important;
  }
  @media (prefers-reduced-motion: reduce) {
    .hv-link, .hviz.is-linking .hv-link {
      stroke-dashoffset: 0 !important; stroke: var(--accent) !important;
      opacity: 1 !important; animation: none !important;
    }
    .hv-cap, .hv-item,
    .hviz.is-linking .hv-cap, .hviz.is-linking .hv-item {
      opacity: 1 !important; transform: none !important; animation: none !important;
    }
  }
}
/* ===== end ===== */

/* ===== v20g — the hero stops forcing a screen, and the seam closes =======
   Owner: "idk if i want the hero vertically aligned … the connection line on
   the left is broken and the space to the next section needs to be seamless."
   The two are the same fault, so they get one fix.

   min-height:100vh forced the hero to a full screen whether or not it needed
   one. At 1600x900 the copy stack is 616px and the padding 214, so the section
   held 830px of content inside a 900px box and distributed the other 70px as
   slack above and below. That slack is the "space to the next section": dead
   air the layout never asked for, sitting exactly where the eye expects the
   page to continue.

   Removing it also fixes the Thread. .hero-thread ends at the container's
   bottom plus --sec-gap; with 70px of flex slack pushing the container UP
   inside an over-tall section, the spine stopped short of the hero's own
   bottom edge and the page thread's unlit track picked up below it — dark line,
   gap, pale line. Let the hero size to its content and the two are continuous
   again, with the overlap the seam was designed around.

   The padding stays exactly as v20f set it: 86 + 64 above, 64 below, so the
   breathing room under the nav is untouched. What goes is only the instruction
   to be a screen tall.
   ====================================================================== */
@media (min-width: 1100px) {
  .hero { display: block; min-height: 0; }
  .hero > .container { width: auto; }
}

/* ---- v20d: THE .ht-fill MASK IS REMOVED --------------------------------
   It was added to soften what looked like a break at the hero seam. That
   reading was taken at scroll 0, where the seam sits below the fold and there
   is nothing to see.

   Sampled properly — scrolled to the seam, darkest pixel across the line every
   10px — the truth was the opposite. #how's own thread lights as you scroll, so
   by the time the seam is on screen there are TWO dark segments meeting, and
   the fade was punching a pale hole between them:

       y 300-370    69   hero's lit fill
       y 390       145   <- the fade
       y 400       206   <- a near-white gap in the middle of a dark line
       y 410-550    96 -> 69   #how's lit fill
       y 560+      202   unlit track

   The line was already continuous; the fix was the defect. */


/* ===== v20h — the objects float ==========================================
   Owner: "can those items float up and down, gentle subtle so it looks lively?"

   This is a LOOP, and this build has argued against loops twice — correctly,
   both times, for objects that were making an argument. It is allowed here for
   one reason: these five are an illustration, not a claim, and a drawing of a
   system that is running should look like it is running. The discipline is in
   the amplitude and in the fail-open, not in refusing it.

   THE MECHANIC THAT MAKES IT COMPOSE. The float uses `translate`, NOT
   `transform`. They are separate properties that both apply, so:
     · the entrance keeps `transform` (hvIn's translateY + opacity)
     · the hover keeps `transform` (translateY(-9px) scale(1.06))
     · the float owns `translate` and never collides with either
   Without that split the float would outrank the hover — an animation's
   `forwards` fill beats a hover rule — and the objects would go dead to the
   pointer, which is the bug v20b's comment already warns about.

   IT STARTS ONLY ONCE THE PAGE HAS SETTLED. Gated on [data-linked], which JS
   sets on a timer at 2160ms regardless of what any animation does. Before that
   the entrance owns the element; after it, the float does. Never both.

   FIVE PERIODS, NO COMMON FACTOR (6.5 / 7.9 / 7.1 / 8.7 / 7.5s). Equal periods
   would make five objects rise and fall as one mass, which reads as the whole
   panel breathing — a screensaver. Deliberately mismatched, they read as five
   things suspended in the same air.

   THE DELAYS ARE POSITIVE, AND THAT IS THE FIX FOR THE HOP. They used to be
   negative (-1.9s, -4.2s ...) to start each object mid-cycle so none waited its
   turn. But a negative delay means the animation's FIRST rendered frame is
   already partway through — so at the instant [data-linked] lands, an object
   sitting at translate 0 was snapped to -5px or -7px in one frame. That is the
   abrupt jump. Positive delays start every object from 0, which is exactly
   where the entrance left it, so the float begins from stillness. The five
   separate within a couple of seconds anyway because the periods do not
   share a factor.

   AMPLITUDE IS 11-14px, doubled from the original 5-7px at the owner's request
   so the drift is actually noticeable. It is now level with the hover's 12px
   lift rather than half it, so the hover leans on scale (1.06) and the deeper
   cast to stay distinguishable — which it still does, measured. */
@media (min-width: 1100px) {
  .hviz[data-linked] .hv-i1 { animation: hvFloatA 6.5s ease-in-out 0s    infinite; }
  .hviz[data-linked] .hv-i2 { animation: hvFloatB 7.9s ease-in-out 0.30s infinite; }
  .hviz[data-linked] .hv-i3 { animation: hvFloatA 7.1s ease-in-out 0.60s infinite; }
  .hviz[data-linked] .hv-i4 { animation: hvFloatB 8.7s ease-in-out 0.90s infinite; }
  .hviz[data-linked] .hv-i5 { animation: hvFloatA 7.5s ease-in-out 1.20s infinite; }

  /* two curves, not one: B is shallower and drifts a touch sideways, so the
     five do not all trace the same vertical line */
  @keyframes hvFloatA {
    0%, 100% { translate: 0 0; }
    50%      { translate: 0 -14px; }
  }
  @keyframes hvFloatB {
    0%, 100% { translate: 0 0; }
    50%      { translate: -4px -11px; }
  }

  /* THE FLOAT KEEPS RUNNING UNDER THE POINTER. It used to pause, on the theory
     that a thing you are looking at directly should hold still — wrong twice.
     Pausing freezes the drift at whatever sub-pixel it reached, and resuming on
     pointerleave restarts from there while the hover transform is transitioning
     back; two clocks, visible stutter. It is also unnecessary: the float owns
     `translate` and the hover owns `transform`, separate properties that
     compose, so they were never fighting for the same pixels. */

  /* fail-open: no float without JS (the gate never opens), none under reduced
     motion, none after the watchdog. All three rest at translate: 0. */
  .js-fallback .hv-item { animation: none !important; translate: none !important; }
  @media (prefers-reduced-motion: reduce) {
    .hviz[data-linked] .hv-item,
    .hviz[data-linked] .hv-item:hover { animation: none !important; translate: none !important; }
  }
}

/* ===== v20i — the nav shell sits in the middle of its band ===============
   Measured: the band is 90px, the shell 66px, and the padding is 20 above and
   4 below. The shell was sitting 16px low in its own band, and because
   .nav::after draws the hairline at the band's BOTTOM, the gap under the shell
   was a quarter of the gap above it — which is what reads as "not centred".

   Equal padding in both states. The stuck state still tightens (90 -> 82), so
   the nav's scroll transition is unchanged in feel; it just tightens
   symmetrically now instead of only from the top.

   --hero-nav MOVES WITH IT. It is the measured underside of the shell and the
   hero's top padding is derived from it, so leaving it at 86 after the shell
   rose to 78 would have silently added 8px of clearance under the nav and
   broken the top/bottom symmetry v20f verified. This is exactly the
   re-measurement that token was named to make obvious. */
.nav          { padding-top: 12px; padding-bottom: 12px; }
.nav.is-stuck { padding-top: 8px;  padding-bottom: 8px;  }

@media (min-width: 1100px) {
  .hero { --hero-nav: 78px; }   /* 12 + 66 = the shell's underside at rest */
}

/* ---- v20j: the caption band moves 1276 -> 1400 --------------------------
   Widening the viewBox 480 -> 540 to separate the two columns is free at 1600,
   where the drawing is HEIGHT-bound (scale stayed 0.962, image width unchanged
   at 108px). It is not free below ~1400, where the drawing becomes WIDTH-bound
   and the extra 60 units are paid for in scale: 0.997 -> 0.887 at 1280.

   The captions are the part of that shrink you actually read, so the larger
   user-unit band now covers everything below 1400 instead of below 1276. Same
   rendered-size logic v20f introduced, re-split at the width where the binding
   constraint changes rather than at the width where it used to. */
@media (min-width: 1100px) {
  .hv-cap { font-size: 17.4px; }
  .hv-sub { font-size: 13.9px; }
}
@media (min-width: 1400px) {
  .hv-cap { font-size: 16.6px; }
  .hv-sub { font-size: 13.2px; }
}

/* ---- v20a: 10px more air above the hero ---------------------------------
   Added to the TOP only. --hero-air feeds both the top and the bottom, so
   raising the token would have moved the foot as well and broken the symmetry
   v20f verified; this is the one place the two are deliberately not equal. */
@media (min-width: 1100px) {
  .hero { padding-top: calc(var(--hero-nav) + var(--hero-air) + 10px); }
}

/* ===== v20b — the illustration becomes WIDTH-bound =======================
   Owner: "increase 20% the size of the bottom 3 images … increase the height of
   the hero section if you need."

   That second sentence is what makes the first one possible. Until now the
   drawing was HEIGHT-bound: v20f took the <svg> out of flow and pinned it to
   the box the COPY leaves, which was right at the time — it stopped the
   illustration inflating the grid rows and silently turning an authored 22px
   gap into 47.6px. But it also meant every extra unit of drawing height was
   paid straight back in scale, so "make these bigger" returned nothing. The
   +50% test measured exactly that: 50% more in user units bought 21% on screen,
   and a 27% caption bump bought 3%.

   Back in flow with an explicit aspect-ratio, the height FOLLOWS the width.
   Scale is then boxWidth / viewBoxWidth and is completely independent of how
   tall the chain is — so the bottom three can grow, the frame can get taller,
   and nothing shrinks. The hero grows instead, which is what was authorised.

   THE ROW-INFLATION BUG DOES NOT COME BACK, and this is the part worth reading:
   it was never caused by the illustration being in flow, it was caused by the
   spanned tracks DISTRIBUTING the excess. align-self: start on the two copy
   items means a taller row leaves its slack at the bottom instead of pushing
   the credit away from the CTA. The authored 32px gap stays 32px. */
@media (min-width: 1100px) {
  .hviz-net {
    position: static; inset: auto;
    width: 100%; height: auto;
    aspect-ratio: 722 / 810;          /* the viewBox's own ratio */
  }
  .hviz { align-self: start; }
  /* the fix that keeps v20f's measured result while allowing the row to grow */
  .hero-copy, .hero-credit { align-self: start; }
}


/* ---- v20b: caption sizes re-derived for the wider frame -----------------
   The frame went 552 -> ~665 user units wide, so at the old 16.6/13.2 the
   captions rendered at 12.4/9.9px — unreadable. Sizes are in user units and
   rendered size is size x scale, so they have to be re-derived every time the
   frame changes. 21/16.6 puts them back at ~16.7/13.2 rendered, which is where
   v20f established them. */
@media (min-width: 1100px) {
  .hv-cap { font-size: 21px; }
  .hv-sub { font-size: 16.6px; }
}
@media (min-width: 1400px) {
  .hv-cap { font-size: 21px; }
  .hv-sub { font-size: 16.6px; }
}


/* ---- v20b: the eyebrow moves under the status pill ----------------------
   v20f stacked .hero-meta between 1100 and 1300 because the pill is a fixed
   292.6px and the 96-character eyebrow was being set into whatever was left —
   21.9ch at 1100, four ragged lines beside a 44px pill. The owner now wants it
   stacked at every width, which also retires the last measure problem in the
   column: the eyebrow gets the full copy width instead of the remainder.

   The 1100-1300 rule above is now redundant and is left in place only so the
   two blocks stay diffable; this one is later and wins everywhere. */
@media (min-width: 1100px) {
  .hero-meta { display: block; }
  .hero-meta .status-pill { margin-bottom: 14px; }
}

/* ===== v20c — the illustration takes the right gutter ====================
   Owner's markup boxed the empty margins either side of the hero. The right
   one is available. The left one is not, and the reason matters:

   .hero-thread is positioned from the CONTAINER's left edge, and it is
   collinear with the page-wide thread in every section below by construction —
   both resolve from --pad and --thr-inset against the same container width.
   Widening the hero's container to reach the left gutter would move the hero's
   spine left of every other section's and put a visible jog at the #how seam,
   which is the exact bug HANDOFF.md records being fixed once already. The left
   margin is the price of the Thread, and the Thread is the signature.

   The right side has no such tie. The illustration bleeds past the container's
   content edge to within 40px of the viewport, which is the biggest void on the
   screen and the one sitting directly beside the thing that wanted more room.

   THE 40px IS NOT DECORATIVE: 100vw includes the scrollbar, so a bleed measured
   from it will run under the scrollbar on Windows unless something covers the
   ~15px. 40 covers it and still reads as a margin. max(0px, ...) means the
   bleed simply stops existing once the viewport is narrower than the container
   rather than going negative and pulling the drawing inward. */
@media (min-width: 1100px) {
  .hviz {
    --hero-bleed: max(0px, (100vw - var(--container)) / 2 + var(--pad) - 96px);
    /* WIDTH IS EXPLICIT, not a negative margin alone. v19d set `width: 100%`,
       which pins the box to its grid track and overrides the stretch a negative
       margin would otherwise cause — so the margin moved the box right without
       widening it, and the bleed bought 21px instead of 136. Stating the width
       as track + bleed makes the intent unambiguous and survives whatever the
       track resolves to. */
    width: calc(100% + var(--hero-bleed));
    /* CAPPED. Uncapped, the bleed scales with the viewport and the drawing runs
       away — measured 845px wide and 24.6px captions at 1920, i.e. bigger than
       1:1 and louder than the headline. 640 is the width at which the drawing's
       own height (810 x scale = 718px) still fits inside a 900px screen once
       the hero's 138px of padding is counted, so the whole hero stays on one
       screen at 1600x900 while the panels get their increase. */
    max-width: 640px;
    margin-right: calc(-1 * var(--hero-bleed));
  }
}

/* ===== v20d — narrower gutters, wider content ===========================
   Owner: "decrease the padding on left and right … some of the items on the
   bottom have been wider too."

   --pad drops 96 -> 64 at the widest band. It is the page's single horizontal
   gutter token, so this widens the content column in EVERY section at once,
   which is what fixes the hero looking narrower than the sections under it.

   THE THREAD SURVIVES, and this is why it is safe to touch --pad at all:
   --thread-x is calc(var(--pad) - var(--thr-inset)), and the page-wide thread
   under #how resolves from the same two tokens against the same container. Both
   move by exactly the same 32px, so they stay collinear by construction. This
   is the one horizontal number on the page that can be changed without
   re-measuring the seam — every other approach to widening the content (a
   bigger --container, a per-section override) would have moved the hero's spine
   out of line with the rest of the page. */
@media (min-width: 1181px) {
  :root { --pad: 64px; }
}


/* ---- v20d: close the gap in the middle ----------------------------------
   The illustration was RIGHT-aligned inside its column. v19d gave .hviz
   `margin-left: auto`, and once max-width clamps the box at 640 that auto
   margin pushes it to the far side of its track — measured at 1920 the track
   starts at x=1039 and the drawing started at 1184, so 145px of the "gap in the
   middle" was margin doing nothing, on top of the 40px column gutter.

   justify-self: start puts the box at the start of its own track, which is
   where the copy stops. The slack moves to the outside edge instead, which is
   the correct place for it on a wide screen — an outer margin reads as margin,
   an inner one reads as a hole. */
@media (min-width: 1100px) {
  .hviz { justify-self: start; margin-left: 0; }
  .hviz-net { aspect-ratio: 749 / 906; }   /* follows the widened viewBox */
}

/* ===== v20 — RETAINERS ===================================================
   The three builds are a sticky card stack; retainers are a different unit —
   monthly, ongoing, and per the pricing doc the place the margin actually
   lives. A fourth card would have implied they are an alternative to a build
   when they are what follows one, so they get a strip: one row, three items,
   no sticky behaviour, sitting between the stack and its footnote.

   Amounts carry the same data-aud / data-usd pair as the cards, so the existing
   currency toggle drives them with no JS change. The `/ month` suffix is a
   .pc-per span because initCurrency() replaces textContent and re-appends only
   that element — any other child would be wiped on the first toggle.
   ====================================================================== */
.retainers {
  margin: 72px 0 0;
  padding: 40px 0 0;
  border-top: 1px solid var(--hairline);
}
.ret-head {
  margin: 0 0 28px;
  font-size: clamp(22px, 2.1vw, 30px); line-height: 1.2; letter-spacing: -.02em;
}
.ret-list {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 32px; margin: 0; padding: 0; list-style: none;
}
.ret-list li { display: flex; flex-direction: column; gap: 6px; }
.ret-name {
  font-size: 13px; font-weight: 500; letter-spacing: .09em; text-transform: uppercase;
  color: var(--tertiary);
}
.ret-amt {
  font-size: clamp(26px, 2.4vw, 34px); font-weight: 500; letter-spacing: -.02em;
  color: var(--text);
}
.ret-amt .pc-per { font-size: 15px; font-weight: 400; color: var(--tertiary); letter-spacing: 0; }
.ret-note { margin: 2px 0 0; font-size: 15px; line-height: 1.5; color: var(--muted); max-width: 34ch; }
.ret-terms { margin: 28px 0 0; font-size: 14px; line-height: 1.55; color: var(--tertiary); }

@media (max-width: 900px) {
  .retainers { margin-top: 56px; padding-top: 32px; }
  .ret-list { grid-template-columns: 1fr; gap: 26px; }
  .ret-note { max-width: none; }
}
