/* =============================================================================
   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 ===== */
