/* ============================================================
   MATTERA v3 · section layout
   Written fresh for the light editorial direction. The JS-driven
   stage internals live in v3-stages.css (extracted from v2); this
   file is the chrome and composition around them.
   ============================================================ */

/* ============================ HERO ============================ */
.hero {
  position: relative;
  display: flex;
  align-items: stretch;
  overflow: hidden;
  min-height: var(--hero-h);
  background: var(--paper);
}

.hero-grid {
  position: relative;
  z-index: 2;
  width: 100%;
  display: grid;
  grid-template-columns: 1.02fr .98fr;
  align-items: stretch;
}
/* The heading-to-trust span is what the dial's box is anchored to, so it is
   set EXPLICITLY rather than emerging from centring. Previously the h1 carried
   `margin: auto 0 0` and the trust line `margin-top: auto`, which split the
   free space 50/50 between them — so any change to padding moved the span by
   only half the amount, and the two anchors drifted in opposite directions.
   Now: the heading sits at a stated distance below the nav, the trust line at
   a stated distance above the hero's foot, and the span between them is
   therefore directly controllable — which is what lets the dial reach v2's
   size without cramming the drawing inside a fixed box. */
.hero-left {
  position: relative;
  z-index: 1;                       /* paint above .hero-right so the form
                                       button never loses hit-testing to the
                                       positioned column (v2 bug, kept fixed) */
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: calc(var(--nav-h) + var(--hero-gap-top))
           clamp(28px, 4vw, 68px)
           var(--hero-gap-bot)
           var(--gutter);
}

.hero-right {
  position: relative;
  display: flex;
  padding: 132px var(--gutter) 0 clamp(8px, 2.1vw, 24px);
}

/* Auto margins split the column's free space above the headline and below
   the form, so the block floats optically centred while the trust line
   stays pinned to the hero's bottom edge. */
/* Line-height is raised here, and ONLY here — h1.display sets .99 globally,
   which is right for an all-lowercase setting but too tight once the words are
   capitalised: line 2's descenders ("Intelligence") now run at a cap-height
   line 3 ("PAGA Defense") instead of at x-height letters. Scoped to the hero
   so no other display heading changes. */
.hero h1 { margin: 0; max-width: 15ch; line-height: 1.06; }
/* PAGA is set upright and in the brand blue, not italic. */
.hero h1 em { font-style: normal; color: var(--brand-blue); }
/* Justified so the right edge runs straight against the hero's left rule
   instead of ragging. `text-align-last: left` keeps the final line from being
   stretched across the measure, and hyphenation stops justification from
   opening rivers of white space at this 47ch measure. */
.hero-sub {
  margin-top: clamp(18px, 2.65svh, 26px);
  max-width: 47ch;
  text-align: justify;
  text-align-last: left;
  hyphens: auto;
  -webkit-hyphens: auto;
}

/* ---- email capture ----
   No overflow:hidden — it can clip the submit button's hit area at the
   container edge in some Chromium/WebKit builds. */
.hero-form {
  /* svh-scaled so the column can tighten on short viewports rather than
     pushing the hero past the fold. At ~982svh these resolve to exactly the
     old fixed values (26 / 42 / 46), so nothing changes on the sizes already
     signed off — they only give way when the screen is genuinely short. */
  margin-top: clamp(28px, 4.28svh, 42px);
  display: flex;
  align-items: stretch;
  max-width: 520px;
  border: 1px solid var(--line-strong);
  background: var(--paper);
}
.hero-form input {
  flex: 1; min-width: 0;
  background: transparent; border: none; outline: none;
  color: var(--ink);
  font-family: var(--sans); font-size: 15px;
  padding: 0 20px; height: 58px; cursor: text;
}
.hero-form input::placeholder { color: var(--ink-4); }
.hero-form button {
  border: none;
  background: var(--btn-bg); color: var(--btn-fg);
  font-family: var(--mono); font-size: 12px; font-weight: 500;
  letter-spacing: .1em; text-transform: uppercase;
  padding: 0 24px; height: 58px;
  display: inline-flex; align-items: center; gap: 12px;
  white-space: nowrap; cursor: pointer;
  transition: background .25s;
}
.hero-form button:hover { background: var(--btn-bg-hover); }
.hero-form button .arr { width: 17px; height: 17px; transition: transform .4s var(--ease-out); }
.hero-form button:hover .arr { transform: translateX(4px); }
.hero-form.is-submitting { pointer-events: none; opacity: .65; }


/* Validation nudge and submit result share one treatment so they read as
   the same component; the border colour carries the state. */
.hero-input-tip,
.hero-form-msg {
  display: block; max-width: 520px;
  margin-top: 16px; padding: 13px 17px;
  font-family: var(--mono); font-size: 12px;
  letter-spacing: .03em; line-height: 1.55;
  border: 1px solid var(--line-strong);
}
.hero-input-tip[hidden], .hero-form-msg[hidden] { display: none; }
.hero-input-tip           { color: var(--warn);  border-color: var(--warn);  background: rgba(180,100,47,.05); pointer-events: none; }
.hero-form-msg.is-success { color: var(--ember); border-color: var(--ember); background: rgba(62,143,82,.05); }
.hero-form-msg.is-error   { color: var(--warn);  border-color: var(--warn);  background: rgba(180,100,47,.05); }

/* No margin-top:auto. An auto margin swallows ALL free space before
   justify-content is consulted, which is why space-between appeared to do
   nothing and the whole surplus kept collecting in this one gap. */
.hero-trust { margin-top: 0; padding-top: clamp(30px, 4.68svh, 46px); }
.hero-trust .lbl {
  font-family: var(--mono); font-size: 10.5px;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-4);
}

/* ---- radial diagram ----
   Geometry is v2's, untouched. Only the stroke tokens change, so the
   blueprint dial becomes a fine navy technical drawing on paper. */
.radial {
  position: absolute;
  /* --gutter, NOT --frame. --frame only reaches the rail; every other piece of
     right-hand content (nav CTA, section text) stops at --gutter. */
  right: var(--gutter);

  /* The box is anchored to two pieces of type in the OTHER column: its top wall
     to the topmost pixel of "Record-backed", its bottom wall to the bottommost
     pixel of the trust line. Those anchors cannot be expressed in CSS — the
     heading's position depends on viewport WIDTH (font-size is a vw clamp) as
     well as height, so any svh-only formula drifts (measured: +/-8px). v3.js
     measures the real glyph bounds and writes these two custom properties.
     The fallbacks are the best static approximation, used only if JS is off. */
  top:    var(--dial-top, 28.9svh);
  bottom: var(--dial-bottom, 58px);

  /* Height comes from top/bottom; the dial is square, so width follows. Width
     must stay auto or the box becomes over-constrained and aspect-ratio is
     ignored. */
  width: auto;
  aspect-ratio: 1 / 1;

  /* Ceiling on that derived width. Height is a function of the VIEWPORT'S
     HEIGHT, width then copies it — so on a tall, narrow viewport (iPad Pro
     portrait, 1024x1366) the square grew to 550px inside a 502px column and
     sat on top of the headline and the lead: measured -59px of clearance,
     i.e. the copy was underneath it and unreadable. It degrades over roughly
     961-1110px of width, above the 960px point where the mobile layout takes
     over and below the width at which the column is simply wide enough.

     Expressed against the viewport rather than the column because the dial is
     MEANT to overhang the column edge slightly at desktop sizes (measured
     15-23px at 1680-1920) — capping at the column would shrink the dial at
     the very sizes it was tuned for. 53vw is the h1's right edge (a stable
     0.47 x vw across every width measured), so what this really says is: never
     come within 28px of the headline.

     Because max-height makes the height definite, top/height/bottom are
     over-constrained and CSS drops `bottom` — which is exactly the right
     casualty. The box keeps its top anchor and stays square; it just stops
     reaching the trust line. Verified below 1280px only; at 1280+ the cap
     never binds and the tuned geometry is untouched. */
  max-height: calc(53vw - var(--gutter) - 28px);
  pointer-events: none;
}

/* The frame. Drawn on a pseudo-element rather than as a border on .radial so
   it cannot alter the SVG's layout box — the arcs still terminate exactly on
   the wall instead of a pixel inside it. The viewBox bounds the labels as well
   as the dial, so everything sits inside the rule. */
.radial::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Deliberately a DARK edge, not var(--line): --line is now cream-on-navy
     (see the scope above) and would be invisible where the panel meets white.
     This crisps the boundary so the panel sits on the page rather than
     glowing off it. */
  border: 1px solid rgba(13, 24, 38, .38);
  pointer-events: none;
  z-index: 2;
}

.radial svg {
  position: relative; z-index: 1;
  width: 100%; height: 100%; overflow: visible; display: block;
  pointer-events: none;
  /* Ring paths end exactly at the viewBox's right edge; their centred
     strokes would bleed a half-stroke past it. Clip ONLY that edge. */
  clip-path: inset(-50% 0 -50% -50%);
}
/* Every stroke derives from the dial's own `color`, so flipping that one
   property inverts the whole diagram for the blue variant — no duplicated
   rules, and the inline <pattern> hatch follows via currentColor too. */
.radial {
  /* Inside the box the dial stands on v2's canvas, so it takes v2's palette
     EXACTLY — the literal values from assets/mattera-v2.css, not v3's
     dark-tone approximations. That is what makes the panel read as a piece of
     v2 set into the page rather than a re-interpretation of it. */
  --ink:         #DDE8F8;   /* v2 --cream    */
  --ink-2:       #B8CCEA;   /* v2 --cream-2  */
  --ink-3:       #7EA4C8;   /* v2 --sage     */
  --ink-4:       #5D84A8;   /* v2 --sage-dim */
  --line:        rgba(221,232,248,.16);
  --line-soft:   rgba(221,232,248,.09);
  --line-strong: rgba(221,232,248,.30);
  --ember:        #4A9E5A;
  --ember-bright: #45C66F;
  color: var(--ink);

  /* Each element's RESTING opacity, named once. The reveal below fades
     stroke-opacity/fill-opacity from 0 back to these, so the values are not
     duplicated between the resting and revealed states.
     These are v2's ACTUAL alphas, not approximations: v2 drew these strokes as
     rgba(cream, a) via --line (.16) and --line-strong (.30), with the wedge at
     .50. v3 draws them as currentColor + stroke-opacity, so the numbers have
     to be transcribed rather than re-invented. The wedge sitting at .42 was
     16% weaker than v2 before hover was even involved. */
  --o-outline: .16;    /* v2 --line        */
  --o-arc: .30;        /* v2 --line-strong */
  --o-arc-dash: .16;   /* v2 --line        */
  --o-spoke: .16;      /* v2 --line        */
  --o-spoke-lit: .30;  /* v2 --line-strong */
  --o-lead: .30;       /* v2 --line-strong */
  --o-wedge: .50;      /* v2 .r-wedge      */
}

/* The shader ground. v2 uses #1D3556 here and calls it the PaperTexture's
   settled tone; sampling the composited result at 1440 gives rgb(31,60,95) —
   #1F3C5F — so that is what this uses. The box is therefore already the exact
   final value on first paint and the WebGL layer composites onto it with no
   shift at all. It doubles as graceful degradation: with no WebGL the panel is
   flat navy of precisely the right value under the grain, never broken and
   never a different colour from the shader version. */
.radial-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: #1F3C5F;
  overflow: hidden;
}
.radial-bg > * { width: 100%; height: 100%; display: block; }

/* v2's paper grain — same tile, same .04 / overlay it uses page-wide. */
.radial-bg::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  opacity: .04;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.r-arc       { fill: none; stroke: currentColor; stroke-opacity: var(--o-arc); stroke-width: 1; }
.r-arc.dash  { stroke-dasharray: 3 5; stroke-opacity: var(--o-arc-dash); }
.r-spoke     { stroke: currentColor; stroke-opacity: var(--o-spoke); stroke-width: 1; }
.r-spoke.lit { stroke-opacity: var(--o-spoke-lit); }
.r-outline   { fill: none; stroke: currentColor; stroke-opacity: var(--o-outline); stroke-width: 1; stroke-dasharray: 4 5; }
.r-wedge {
  fill: url(#hatch); stroke: currentColor; stroke-width: 1; stroke-opacity: var(--o-wedge);
  pointer-events: all;                 /* the only hit-testable part of the dial */
  transition: stroke .3s var(--ease), stroke-opacity .3s var(--ease), stroke-width .3s var(--ease);
}
/* Hover: the wedge lights up green.
   The second selector is not redundant. v2 could write this as a bare
   `.r-wedge:hover` because its reveal animated `opacity`, a DIFFERENT property
   — no contest. v3's reveal animates `stroke-opacity` (to keep hairlines crisp
   mid-fade), so the revealed-state rule
       html.js-fx .radial.in svg .r-wedge   -> specificity (0,4,2)
   outranks a bare
       .r-wedge:hover                       -> specificity (0,2,0)
   and pinned the hover outline at --o-wedge (0.42) instead of 1. That is why
   the green read thin and washed out next to v2's. Matching the reveal's
   specificity and adding :hover puts it back at full strength. */
.r-wedge:hover,
html.js-fx .radial.in svg .r-wedge:hover {
  fill: url(#hatch-on);
  stroke: var(--ember-bright);
  stroke-width: 1.5;
  stroke-opacity: 1;
}
.r-lead  { stroke: currentColor; stroke-opacity: var(--o-lead); stroke-width: 1; }
.r-mark  { fill: currentColor; }
.r-label { fill: var(--ink-2); font-family: var(--mono); font-size: 13px; letter-spacing: .04em; }
.r-label .pct { fill: var(--ink-4); }

/* Staged draw-on. The radial is deferred by v3.js until the hero text has
   settled — its many concurrent SVG transitions repaint the shared layer and
   would stutter the text animation running alongside.

   The fade animates stroke-opacity / fill-opacity, NOT opacity. Animating
   `opacity` on an SVG element makes the browser render it into an offscreen
   transparency buffer and composite it, and 1px strokes get resampled through
   that buffer — measured at 2x DPR, edges spanned 3-4 device pixels mid-fade
   against 2-3 once settled, which is the "blurry while loading, snaps sharp at
   the end" artefact. stroke-opacity/fill-opacity modulate the paint on the
   shape itself: no transparency group, no resample, identical rasterisation
   from the first frame to the last. */
.radial { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  html.js-fx .radial svg .r-outline,
  html.js-fx .radial svg .r-arc,
  html.js-fx .radial svg .r-spoke,
  html.js-fx .radial svg .r-lead {
    stroke-opacity: 0;
    transition: stroke-opacity .7s var(--ease-out);
  }
  html.js-fx .radial svg .r-wedge {
    stroke-opacity: 0; fill-opacity: 0;
    transition: stroke-opacity .7s var(--ease-out), fill-opacity .7s var(--ease-out);
  }
  html.js-fx .radial svg .r-mark,
  html.js-fx .radial svg .r-label {
    fill-opacity: 0;
    transition: fill-opacity .7s var(--ease-out);
  }

  html.js-fx .radial.in svg .r-outline                 { stroke-opacity: var(--o-outline);  transition-delay: .25s; }
  html.js-fx .radial.in svg .r-arc.dash:nth-of-type(1) { stroke-opacity: var(--o-arc-dash); transition-delay: .38s; }
  html.js-fx .radial.in svg .r-arc.dash:nth-of-type(2) { stroke-opacity: var(--o-arc-dash); transition-delay: .51s; }
  html.js-fx .radial.in svg .r-arc:not(.dash)          { stroke-opacity: var(--o-arc);      transition-delay: .64s; }
  /* spokes 1-2 carry .lit, 3-4 do not — restore each to its own resting value */
  html.js-fx .radial.in svg .r-spoke:nth-of-type(1)    { stroke-opacity: var(--o-spoke-lit); transition-delay: .77s; }
  html.js-fx .radial.in svg .r-spoke:nth-of-type(2)    { stroke-opacity: var(--o-spoke-lit); transition-delay: .86s; }
  html.js-fx .radial.in svg .r-spoke:nth-of-type(3)    { stroke-opacity: var(--o-spoke);     transition-delay: .95s; }
  html.js-fx .radial.in svg .r-spoke:nth-of-type(4)    { stroke-opacity: var(--o-spoke);     transition-delay: 1.04s; }
  html.js-fx .radial.in svg .r-wedge { stroke-opacity: var(--o-wedge); fill-opacity: 1; transition-delay: 1.3s; }

  /* Once the draw-on has finished, v3.js adds .drawn and the stagger stops
     applying. Without this the reveal's 1.3s transition-delay also governed
     the HOVER transition, so the wedge took 1.3s + 0.7s = 2s to light up and
     just as long to fade back — which is most of why the green felt dead
     compared with v2. v2 never hit this because its reveal animated `opacity`
     and left `transition` free for the hover. */
  html.js-fx .radial.drawn svg .r-wedge {
    transition: stroke .22s var(--ease), stroke-width .22s var(--ease),
                stroke-opacity .22s var(--ease), fill-opacity .22s var(--ease);
    transition-delay: 0s;
  }

  html.js-fx .radial.in svg .r-lead:nth-of-type(5)  { stroke-opacity: var(--o-lead); transition-delay: 1.5s; }
  html.js-fx .radial.in svg .r-mark:nth-of-type(1),
  html.js-fx .radial.in svg .r-label:nth-of-type(1) { fill-opacity: 1; transition-delay: 1.5s; }
  html.js-fx .radial.in svg .r-lead:nth-of-type(6)  { stroke-opacity: var(--o-lead); transition-delay: 1.62s; }
  html.js-fx .radial.in svg .r-mark:nth-of-type(2),
  html.js-fx .radial.in svg .r-label:nth-of-type(2) { fill-opacity: 1; transition-delay: 1.62s; }
  html.js-fx .radial.in svg .r-lead:nth-of-type(7)  { stroke-opacity: var(--o-lead); transition-delay: 1.74s; }
  html.js-fx .radial.in svg .r-mark:nth-of-type(3),
  html.js-fx .radial.in svg .r-label:nth-of-type(3) { fill-opacity: 1; transition-delay: 1.74s; }
  html.js-fx .radial.in svg .r-lead:nth-of-type(8)  { stroke-opacity: var(--o-lead); transition-delay: 1.86s; }
  html.js-fx .radial.in svg .r-mark:nth-of-type(4),
  html.js-fx .radial.in svg .r-label:nth-of-type(4) { fill-opacity: 1; transition-delay: 1.86s; }
}

/* ============================ PROBLEM ============================ */
.prob-top {
  display: grid;
  grid-template-columns: 1fr .78fr;
  gap: clamp(36px, 6vw, 92px);
  align-items: end;
  margin-bottom: clamp(56px, 7vw, 96px);
}
.prob-fig { margin: 0; }
.prob-fig img {
  width: 100%; height: auto;
  aspect-ratio: 4 / 3; object-fit: cover;
}

.problem-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--line);          /* the gap IS the rule — one hairline grid */
  border-block: 1px solid var(--line);
}
.problem-card {
  background: var(--paper-2);
  padding: clamp(30px, 3.2vw, 46px) clamp(24px, 2.4vw, 38px) clamp(38px, 4vw, 54px);
}
.pc-num {
  font-family: var(--mono); font-size: 11px; letter-spacing: .18em;
  color: var(--ink-4); display: block; margin-bottom: 26px;
}
.pc-title {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(20px, 1.7vw, 24px); line-height: 1.2;
  letter-spacing: -.008em; color: var(--ink); margin: 0 0 14px;
}
.pc-body { margin: 0; font-size: 15.5px; color: var(--ink-2); }

/* ============================ SOLUTION ============================ */
.show-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(28px, 3.4vw, 56px);
  margin-top: clamp(52px, 6vw, 86px);
}
.show-eyebrow {
  font-family: var(--mono); font-size: 11px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-4); display: block; margin-bottom: 16px;
}
.show-label {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(21px, 2vw, 27px); line-height: 1.18;
  letter-spacing: -.01em; color: var(--ink); margin: 0 0 12px;
}
.show-blurb { margin: 0 0 26px; font-size: 15.5px; color: var(--ink-2); max-width: 46ch; }

/* The stage frame.
   These cards carry v2's animation stages, and those stages were authored as
   pale linework for a navy canvas — on paper their strokes all but vanish.
   Rather than re-tune dozens of alphas, the card itself becomes the blue
   textured panel (.surface-blue in v3.css also opens a local dark scope), so
   every stage reads exactly as designed while the page around it stays white.
   It doubles as the "sprinkle of v2's blues" these sections wanted. */
.glass-card {
  position: relative;
  border: 1px solid var(--glass-edge);
  overflow: hidden;
}

/* ==================== STATEMENT BAND (new in v3) ==================== */
.statement {
  position: relative;
  min-height: min(94svh, 900px);
  display: flex;
  align-items: flex-end;
  padding-block: clamp(64px, 9vw, 128px);
  overflow: hidden;
  border-top: 0;
}
.statement-img { position: absolute; inset: 0; z-index: 0; }

/* ---- CMYK halftone surface ----
   HalftoneCmyk takes the photograph as INPUT and re-renders it as CMYK
   separations on paper, so this layer replaces the frame rather than tinting
   it — no blend mode, and the <picture> beneath is purely the no-WebGL
   fallback. Its grainMixer is what puts speckle into the black ground, which
   is the thing a soft-light paper texture could not do: measured, that
   approach added 0.0% high-frequency detail over a band this wide.

   The canvas spans the full section width, so the grain is continuous across
   the whole band even where the photograph itself is empty ground — the
   texture never stops short of an edge.

   Faded rather than cut, so the ink and the image dissolve together instead of
   the surface outliving what it sits on. Held at opacity 0 until the shader
   has actually rendered, so a slow or blocked mount shows the plain photograph
   instead of a flash of paper.

   The hold runs to 70%, which is measured, not chosen by eye: the hand spans
   51.1%-70.7% of the band (found by skin-tone detection on the un-shaded
   render). The previous curve started easing at 38% and was down to 0.15 by
   the time it cleared the fingers, so the one passage that most needed
   texturing — generated skin — got the least of it. Full strength now carries
   past the fingertips before the taper starts.

   Extending down is free because .statement-fade paints OVER this layer (same
   z-index, later sibling): whatever ink survives here is dissolved into the
   section by the fade regardless, so the two never fight. */
.statement-ink {
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
  opacity: 0;
  transition: opacity .6s var(--ease-out);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0,0,0,.78) 78%, rgba(0,0,0,.34) 86%, transparent 95%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0,0,0,.78) 78%, rgba(0,0,0,.34) 86%, transparent 95%);
}
.statement-ink.is-on { opacity: 1; }
.statement-ink > * { width: 100%; height: 100%; display: block; }
.statement-img img {
  width: 100%; height: 100%; object-fit: cover;
  /* At normal band aspects this crops HEIGHT, so x is inert and only y counts.
     x matters only on narrow/tall viewports, where cover crops width instead:
     49% is this frame's measured luminance centroid, so the figure stays
     centred there rather than sliced. */
  object-position: 49% 50%;
}
/* The top scrim is GONE, and should not come back in this form.
   It existed as insurance for the fixed nav passing over the frame's bright
   shirt. But it was anchored to the SECTION while the nav is anchored to the
   VIEWPORT, so the two only coincided at a single scroll position; everywhere
   else it was a free-floating vignette. Measured: the frame's own luminance is
   flat at 63-69 down the whole band, while the composite ramped 19 -> 64 over
   the top 117px. That ramp was the "drop shadow" at the head of the section.
   If the nav ever does need help over this band, fix it on the NAV — which is
   the thing that actually stays put — via --nav-glass. */
/* The dissolve. The reference never cuts image-to-colour on a hard line; it
   lets the frame fall away into the section colour.

   The heavy lifting now happens upstream, in the build: scripts/build_images_v3.py
   remaps this photograph's black point onto #0D1826 — the exact value of
   --paper here — so even at ZERO fade opacity the frame's ground already IS
   the section colour. Measured on the emitted file: corners rgb(14,25,39) and
   rgb(14,25,37) against the section's rgb(13,24,38). That is what makes the
   join invisible rather than merely soft, and it is why this gradient can now
   be gentler than the one it replaces: it no longer has to hide a seam, only
   to dissolve the figure. The stride survives further down the frame as a
   result. */
.statement-fade {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 58%;
  z-index: 1; pointer-events: none;
  /* Dissolves to the photograph's OWN black, not to the section navy. Fading a
     black ground towards blue is visible as a colour shift across what should
     read as one flat surface — it was the frame appearing to change material
     halfway down. Matching the dissolve to the frame keeps the ground a single
     colour from the top of the band to the bottom. Handing off to the LIGHT
     section that now follows is dealt with separately, on .statement + .caps,
     where there is room to do it over a distance the eye cannot follow. */
  background: linear-gradient(to bottom,
    rgba(0,0,0,0)   0%,
    rgba(0,0,0,.22) 30%,
    rgba(0,0,0,.58) 56%,
    rgba(0,0,0,.88) 78%,
    #000            94%,
    #000            100%);
}

/* Mirror of the dissolve above, resolved onto paper instead of the frame's own
   black. Two jobs, same as its counterpart: it lands the band's foot on the
   page colour, and it lifts the copy off the photograph — measured unscrimmed,
   the lead crossed the manila folder at 2.72:1 and the h2 at 3.02:1, both under
   AA. Through this it sits on ~238 luminance, i.e. ~13:1. */
.statement-fade.is-light {
  background: linear-gradient(to bottom,
    rgba(255,255,255,0)   0%,
    rgba(255,255,255,.30) 30%,
    rgba(255,255,255,.72) 56%,
    rgba(255,255,255,.93) 78%,
    #FFFFFF               94%,
    #FFFFFF              100%);
}

.statement-inner { position: relative; z-index: 2; max-width: 100%; }
/* Over a photograph both the eyebrow and the lead need lifting off the
   section's default muted tones — the scrim eats the low end. */
.statement-inner .eyebrow { margin-bottom: 26px; color: color-mix(in srgb, var(--ink) 72%, transparent); }

.statement-inner h2 { max-width: 17ch; }
.statement-inner .lead { margin-top: 26px; max-width: 52ch; color: color-mix(in srgb, var(--ink) 86%, transparent); }

/* ============ STATEMENT -> PROCESS ============
   The band ends on the photograph's own black; this section's canvas is the
   dark tone's navy. Meeting those on a hard edge would draw a line, so the
   black is carried down into the section and the navy arrives gradually —
   the same 13-38 level shift, spread past where the eye can find an edge. */
.statement + .process {
  position: relative;
  isolation: isolate;
  /* `.tone-dark + .tone-dark` draws a hairline between same-tone sections,
     which is right where two dark sections MEET and wrong where they dissolve.
     Measured rgb(29,39,53) against the band's black — a visible rule sitting
     on the join the gradient below exists to erase. */
  border-top: 0;
}
.statement + .process::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 340px;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(to bottom, #000 0%, rgba(0,0,0,0) 100%);
}

/* ---- one composited layer per tone ----
   The crossfade happens HERE and nowhere else. Everything inside a wrapper is
   static relative to its siblings, so the browser has a single opacity to
   animate per tone rather than one per layer. That is what fixed the
   horizontal seams: a stack of six independently-transitioning full-bleed
   layers gets rasterised in tiles, and mid-scroll adjacent tiles can hold
   different frames of the transition.

   Only the photograph and its halftone need a light twin. The dissolve, the
   scrim and the black handed into .process are dark-only — the band's foot is
   past the fold at flip time and its head only reaches the nav long after the
   page has gone dark — but they now ride the dark wrapper for free rather than
   carrying gates of their own. */
.statement-tone {
  position: absolute; inset: 0;
  z-index: 0;
  pointer-events: none;
  transition: opacity .5s var(--ease);
}
.statement-tone.is-light { opacity: 1; }
.statement-tone.is-dark  { opacity: 0; }
html.tone-page-dark .statement-tone.is-light { opacity: 0; }
html.tone-page-dark .statement-tone.is-dark  { opacity: 1; }

/* The black handed down into .process is the one dark-tone layer outside the
   section, so it keeps its own gate. */
.statement + .process::before {
  opacity: 0;
  transition: opacity .5s var(--ease);
}
html.tone-page-dark .statement + .process::before { opacity: 1; }

.statement-fade.is-light {
  background: linear-gradient(to bottom,
    rgba(255,255,255,0)   0%,
    rgba(255,255,255,.30) 30%,
    rgba(255,255,255,.72) 56%,
    rgba(255,255,255,.93) 78%,
    #FFFFFF               94%,
    #FFFFFF              100%);
}

.statement-inner { position: relative; z-index: 2; max-width: 100%; }
/* Over a photograph both the eyebrow and the lead need lifting off the
   section's default muted tones — the scrim eats the low end. */
.statement-inner .eyebrow { margin-bottom: 26px; color: color-mix(in srgb, var(--ink) 72%, transparent); }

.statement-inner h2 { max-width: 17ch; }
.statement-inner .lead { margin-top: 26px; max-width: 52ch; color: color-mix(in srgb, var(--ink) 86%, transparent); }

/* ============ STATEMENT -> PROCESS ============
   The band ends on the photograph's own black; this section's canvas is the
   dark tone's navy. Meeting those on a hard edge would draw a line, so the
   black is carried down into the section and the navy arrives gradually —
   the same 13-38 level shift, spread past where the eye can find an edge. */
.statement + .process {
  position: relative;
  isolation: isolate;
  /* `.tone-dark + .tone-dark` draws a hairline between same-tone sections,
     which is right where two dark sections MEET and wrong where they dissolve.
     Measured rgb(29,39,53) against the band's black — a visible rule sitting
     on the join the gradient below exists to erase. */
  border-top: 0;
}
.statement + .process::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 340px;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(to bottom, #000 0%, rgba(0,0,0,0) 100%);
}


/* ============================ CAPABILITIES ============================ */
.caps .sec-head { margin-bottom: clamp(44px, 5vw, 72px); }
.caps-body {
  display: grid;
  grid-template-columns: 210px 1fr;
  gap: clamp(28px, 4vw, 64px);
  align-items: start;
}
.caps-rail {
  position: sticky; top: 118px;
  display: flex; flex-direction: column;
  border-left: 1px solid var(--line);
}
.caps-tab {
  display: flex; align-items: baseline; gap: 12px;
  background: none; border: none; text-align: left;
  padding: 14px 0 14px 18px;
  margin-left: -1px;
  border-left: 1px solid transparent;
  color: var(--ink-4);
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.caps-tab-n { font-family: var(--mono); font-size: 10.5px; letter-spacing: .16em; }
.caps-tab-l { font-family: var(--sans); font-size: 15px; font-weight: 500; letter-spacing: -.005em; }
.caps-tab:hover { color: var(--ink-2); }
.caps-tab.is-active { color: var(--ink); border-left-color: var(--ink); }

/* ============================ ASK ============================ */
/* Light now, like everything from here to the footer. The scene's parts are
   absolutely positioned and centred on the stage; the stage itself must span
   the full viewport width for that centring to be true — any inherited
   section padding shifts the whole composition off-axis. */
.ask { padding: 0; }
.ask-track { padding: 0; }
.ask-stage { padding: 0; }
.ask-head { text-align: center; }
.ask-head h2 { max-width: 20ch; margin-inline: auto; }

/* ============================ PROCESS ============================ */
.process .sec-head { margin-bottom: clamp(48px, 5.5vw, 76px); }

/* ---- the dark run meeting paper ----
   There is no resolve gradient here any more, and that is the point. While the
   process/value join is anywhere near the viewport the page carries
   html.tone-page-dark, so --paper on this section and --paper on the next are
   the SAME value: the two sides of the join are literally one colour and there
   is nothing to dissolve. The tone flip does spatially, and for free, what
   ~1300px of hand-tuned ramp could only approximate.
   Deleted with it: the inflated padding-bottom, the +6px background offset that
   stopped the navy leaking at the fractional bottom edge, --paper-soft on the
   receiving section, and its trimmed padding-top. All of those existed only to
   serve the ramp; the section now takes plain --section-y on both sides. */
/* The divider here is RESTORED, deliberately. It was suppressed while a ~1300px
   ramp was dissolving these two sections into each other — a rule across the
   one row the resolve existed to erase. There is no ramp now: the sections
   simply meet, both taking plain --section-y, and two sections that meet get
   the generic `.section + .section:not(.tone-dark)` hairline like every other
   pair on the page. It is token-driven, so it reads dark-on-light and
   light-on-dark without a second rule. */


/* The source is a 3:4 portrait (a hand pinning notes to a document wall).
   Keep close to that ratio — an earlier 21:9 letterbox sliced away the
   subject entirely and landed on blank wall. Slight lift in brightness so
   the black-and-white frame reads against the dark tone. */
.proc-fig {
  margin: 0 0 clamp(56px, 7vw, 100px);
  max-width: min(430px, 44%);
  margin-inline: auto;
}
.proc-fig img {
  width: 100%; height: auto;
  aspect-ratio: 4 / 5; object-fit: cover; object-position: 52% 34%;
  filter: grayscale(1) contrast(1.06) brightness(1.02);
}
.ps-tag {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-4); margin-bottom: 14px;
}
.ps-title {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(19px, 1.7vw, 23px); line-height: 1.2;
  color: var(--ink); margin-bottom: 10px;
}
.ps-body { margin: 0; font-size: 15px; color: var(--ink-2); }
.ps-note {
  margin-top: 16px; padding-top: 14px;
  border-top: 1px solid var(--line);
  font-family: var(--mono); font-size: 11.5px;
  letter-spacing: .06em; color: var(--ember);
}
.pp-eyebrow {
  font-family: var(--mono); font-size: 11px; letter-spacing: .22em;
  text-transform: uppercase; color: var(--ink-4);
  display: block; margin-bottom: 18px;
}
.pp-title {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(24px, 2.6vw, 36px); line-height: 1.14;
  letter-spacing: -.012em; color: var(--ink); margin: 0 0 16px;
}
.pp-line { margin: 0 auto; max-width: 62ch; font-size: 15.5px; color: var(--ink-2); }
.pp-chips {
  display: flex; flex-wrap: wrap; gap: 10px;
  justify-content: center; margin-top: 28px;
}

/* ============================ VALUE ============================ */
.value .sec-head { margin-bottom: clamp(46px, 5.5vw, 78px); }
.val-grid {
  display: grid;
  grid-template-columns: 1.15fr .85fr;
  gap: clamp(32px, 4.5vw, 76px);
  align-items: start;
}
/* Carries .surface-blue in the markup, which is the same island the capability
   and "build the matter" cards use and the same base navy as the hero dial
   (#1D3556) — so the panel reads as part of that family rather than as a third
   surface. No `background` here: the island's own gradient is the point, and a
   token background declared after it would win on source order and flatten it.

   The corner is CUT, which it was not before. .val-fold draws the folded flap
   and its crease across the top-right 22px, but the box underneath was still a
   full rectangle, so the square corner sat behind the flap and read as a second
   triangle. The clip removes exactly the wedge the fold is meant to have torn
   away; the flap's diagonal then IS the edge of the paper. Keep the 22px here
   and the 22 in the SVG's viewBox in step. */
.val-doc {
  position: relative;
  border: 1px solid var(--line);
  padding: clamp(26px, 3vw, 40px);
  clip-path: polygon(0 0, calc(100% - 22px) 0, 100% 22px, 100% 100%, 0 100%);
}
.val-doc-head {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--ink-4);
  padding-bottom: 18px; margin-bottom: 6px;
  border-bottom: 1px solid var(--line);
}
.val-row {
  display: flex; gap: 16px; align-items: baseline;
  padding: 15px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 15.5px; color: var(--ink);
}
.val-row:last-of-type { border-bottom: 0; }
.val-n {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .12em;
  color: var(--ink-4); flex: 0 0 auto;
}
/* No border-top here — item 08's own .val-row border-bottom already provides
   the divider under the list; a second one on the CTA read as a duplicate.
   With the border gone, padding-top no longer earns its keep either — it only
   made sense as breathing room BELOW that border. Kept as margin, tightened
   so the pilot copy sits at natural paragraph distance from the divider. */
.val-doc-cta { margin-top: 22px; }
.val-start {
  display: inline-flex; align-items: center; gap: 10px;
  background: var(--btn-bg); color: var(--btn-fg); border: none;
  font-family: var(--mono); font-size: 11.5px; font-weight: 500;
  letter-spacing: .09em; text-transform: uppercase;
  padding: 14px 22px;
  transition: background .25s var(--ease);
}
.val-start:hover { background: var(--btn-bg-hover); }
.val-start .arr { width: 16px; height: 16px; transition: transform .4s var(--ease-out); }
.val-start:hover .arr { transform: translateX(4px); }
/* dog-ear, echoing the packet motif */
/* -1px, and it has to be: absolute offsets resolve against the PADDING box
   while clip-path resolves against the BORDER box, so with a 1px border the
   two disagree by exactly that much. -1px puts the SVG's 22x22 box on the
   border-box corner, which is where the clip wedge is cut — the flap's
   diagonal then lands on the torn edge instead of 1px inside it. */
.val-fold { position: absolute; top: -1px; right: -1px; width: 22px; height: 22px; }
.vf-flap { fill: var(--paper); }
.vf-line { fill: var(--line-strong); }

.val-gains { display: flex; flex-direction: column; gap: clamp(26px, 3vw, 40px); }
.val-for {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--ink-4);
  display: block; margin-bottom: 14px;
}
.val-item {
  padding: 12px 0; border-top: 1px solid var(--line);
  font-size: 15.5px; color: var(--ink-2);
}

/* ============================ FAQ ============================ */
.faq-layout {
  display: grid;
  grid-template-columns: .8fr 1.2fr;
  gap: clamp(34px, 5vw, 88px);
  align-items: start;
}
.faq-layout .sec-head { position: sticky; top: 122px; }
.faq-item { border-bottom: 1px solid var(--line); }
.faq-item:first-child { border-top: 1px solid var(--line); }
.faq-q {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: 24px; background: none; border: none; text-align: left;
  padding: 26px 0;
  font-family: var(--display); font-weight: 400;
  font-size: clamp(17px, 1.5vw, 21px); line-height: 1.3;
  color: var(--ink);
  transition: color .25s;
}
.faq-q:hover { color: var(--ink-2); }
/* plus/minus built from two rules so it can rotate into a minus */
.faq-q .pm { position: relative; width: 13px; height: 13px; flex: 0 0 auto; }
.faq-q .pm::before, .faq-q .pm::after {
  content: ""; position: absolute; background: var(--ink-3);
  transition: transform .35s var(--ease), opacity .35s var(--ease);
}
.faq-q .pm::before { top: 6px; left: 0; width: 13px; height: 1px; }
.faq-q .pm::after  { left: 6px; top: 0; height: 13px; width: 1px; }
.faq-item.open .pm::after { transform: rotate(90deg); opacity: 0; }
.faq-a { overflow: hidden; height: 0; transition: height .42s var(--ease); }
.faq-a-inner { padding: 0 0 26px; max-width: 64ch; font-size: 15.5px; color: var(--ink-2); }

/* ============================ CLOSING ============================ */
.closing { padding-block: clamp(78px, 9vw, 150px); }
.close-grid {
  display: grid;
  grid-template-columns: 1.1fr .9fr;
  gap: clamp(36px, 6vw, 96px);
  align-items: center;
}
.close-copy h2 { max-width: 19ch; }
.close-copy .lead { margin-top: 22px; max-width: 46ch; }
.close-form { margin-top: 36px; }
.close-fig { margin: 0; }
.close-fig img { width: 100%; height: auto; aspect-ratio: 3/4; object-fit: cover; }

/* ============================ FOOTER ============================ */
/* v2's composition: a tall, open, left-aligned stack sitting on the baseline,
   with the halftone bear anchored right and bleeding past the frame. The
   spaciousness IS the design — the height floor is what buys it. No separate
   band colour; the footer is the same paper as the section above, so the page
   ends without another horizontal seam. */
.footer {
  position: relative;
  background: var(--paper);
  /* No rule between the last section and the footer. The page already changes
     nothing across that boundary — same paper, no tone shift — so a divider was
     asserting a break that isn't there. Open white space separates them now. */
  min-height: clamp(460px, 60vh, 700px);
  display: flex;
  align-items: flex-end;
  padding-block: clamp(132px, 14vw, 216px) clamp(44px, 5vw, 66px);
  overflow: hidden;
}
.footer .container { position: relative; z-index: 1; }

/* Clip to the WINDOW, not the frame. v2 inset this to var(--frame) because v2
   drew visible frame rails for the bear to run behind; v3 has no rails, so the
   same inset just amputates the rear mid-body. overflow:hidden still prevents
   any horizontal scroll. */
/* overflow:hidden is now only a safety net — the bear is sized to fit, so
   nothing should ever reach it. */
.foot-bear-clip { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }

/* The whole animal, tucked to the content edge.
   v2 deliberately ran it off the page (right:-170px, bottom:-7%) because v2 had
   frame rails for it to disappear behind — the crop read as the bear passing
   BEHIND the page's structure. v3 has no rails, so the same crop just reads as
   an amputation. Anchored to --gutter instead: its right edge lands exactly
   where every other piece of right-hand content stops, and nothing is cut.
   The SVG is natively 400x200, so the 2/1 box means the shader neither
   letterboxes nor crops it. */
.foot-bear {
  position: absolute;
  right: var(--gutter);
  bottom: 0;
  /* the min() guard keeps it inside the viewport on narrow screens, so the
     "never cut off" rule holds without relying on the clip */
  width: min(clamp(520px, 50vw, 900px), calc(100% - var(--gutter) - 24px));
  aspect-ratio: 2 / 1;
  opacity: 0.10;                        /* a touch above v2's .08 — navy on white
                                           carries less weight than cream on navy */
}

.foot-stack { display: flex; flex-direction: column; align-items: flex-start; }
/* M logomark only. viewBox is 170.89x86.05 (~1.99:1), so 36x18 keeps it at the
   same physical size the M had inside the combined wordmark. */
.foot-stack .brand-logo { width: 36px; height: 18px; overflow: visible; }

.foot-contact {
  display: inline-block; margin-top: 18px;
  font-family: var(--mono); font-size: 12.5px; letter-spacing: .04em;
  color: var(--ink-2);
  border-bottom: 1px solid var(--line-strong); padding-bottom: 2px;
  transition: color .25s, border-color .25s;
}
.foot-contact:hover { color: var(--ink); border-bottom-color: var(--ink); }

.foot-links { margin-top: clamp(36px, 4vw, 52px); display: flex; flex-direction: column; align-items: flex-start; gap: 0; }
.foot-links a {
  font-family: var(--sans); font-size: 14px; letter-spacing: 0; text-transform: none;
  color: var(--ink-2); padding: 6px 0; transition: color .25s;
}
.foot-links a:hover { color: var(--ink); }

.foot-legal {
  margin-top: clamp(30px, 3.4vw, 44px);
  display: flex; flex-direction: column; gap: 7px;
  border-top: 0; padding-top: 0; width: auto;
  font-family: var(--mono); font-size: 11px; letter-spacing: .03em;
  color: var(--ink-4);
}
.foot-legal b { color: var(--ink-2); font-weight: 500; }

/* ---------- Grid item min-size guard ----------
   A `1fr` track is `minmax(auto, 1fr)`, and that `auto` floor resolves to the
   item's MIN-CONTENT — which a nowrap button or an intrinsically-sized <img>
   can push past the viewport. The track then refuses to shrink and the whole
   document gets wider than the screen (the fixed nav inherits that width, so
   the symptom shows up far from the cause). Setting an explicit 0 floor on
   the grid children lets the track do its job. */
.hero-grid > *,
.close-grid > *,
.prob-top > *,
.val-grid > *,
.faq-layout > *,
.caps-body > *,
.show-grid > * { min-width: 0; }

/* ============================ RESPONSIVE ============================ */
@media (max-width: 1080px) {
  .caps-body { grid-template-columns: 168px 1fr; }
  .val-grid, .faq-layout { grid-template-columns: 1fr; }
  .faq-layout .sec-head { position: static; }
}

/* The showcase pair needs about 1140px before two columns is honest. Under
   that, each stage falls below ~500px wide, the dossier inside splits into two
   ~165px inner columns, and its claim card wraps out to 249px of content — in
   a 16:9 box that is only 199px tall. The foot of the claim and the last record
   chips were being clipped straight off by the card's overflow, silently, on
   every 1024x768 iPad in landscape. Measured: 28px over at 1009, 16px at 1085,
   clear by 1165.

   This lives here rather than in v3-stages.css because that file's attempt at
   it (a 1080px one-column rule) could never fire: `.show-grid` is declared with
   two columns at the top of THIS file, which loads later and so won at every
   width. That dead rule is gone; this is the one that governs. */
@media (max-width: 1150px) {
  .show-grid { grid-template-columns: 1fr; gap: clamp(30px, 4vw, 52px); }
}

@media (max-width: 960px) {
  .hero { min-height: 0; }
  .hero-grid { grid-template-columns: 1fr; }
  .hero-left { padding: 116px var(--gutter) 56px; }
  .hero h1 { max-width: 18ch; }
  /* The dial becomes a wide band beneath the copy rather than a column.
     overflow:hidden is load-bearing: the dial is square and taller than the
     band, so without it the top of the dial rides up over the trust line. */
  .hero-right {
    padding: 0;
    /* Tall enough to fit the dial's labelled outer arc — at a shorter band
       the § callouts crop away and the dial reads as abstract hatching
       rather than as data. */
    height: clamp(330px, 94vw, 480px);
    overflow: hidden;
  }
  /* The dial's content lives in its bottom-right quadrant (the wedge, the
     lit spokes, the § labels); the top-left is empty space. Anchor bottom-
     right and oversize it so the band shows the part worth showing. */
  .radial {
    right: 0; bottom: 0; top: auto; left: auto;
    transform: none;
    width: min(96%, 470px);
  }

  .prob-top { grid-template-columns: 1fr; align-items: start; }
  .prob-fig { order: -1; }
  .prob-fig img { aspect-ratio: 16/9; }
  .problem-grid { grid-template-columns: 1fr; }
  .show-grid { grid-template-columns: 1fr; }
  .close-grid { grid-template-columns: 1fr; }
  .close-fig { order: -1; max-width: 420px; }
  .statement { min-height: 74svh; }
}

@media (max-width: 560px) {
  /* Side-by-side, the input collapses to a few characters and the placeholder
     truncates. Stack instead — the button gets full width, which is also the
     better touch target.

     `flex: 0 0 auto` on both children is LOAD-BEARING, not tidying. The base
     rule gives the input `flex: 1`, i.e. flex-basis: 0. In the row layout that
     basis governs WIDTH and the declared `height: 58px` stands. Turning the
     container into a column swings the main axis: flex-basis now governs
     HEIGHT, silently outranks the declared height, and the input collapses to
     whatever the button leaves over — 21px, a squashed slot you cannot
     comfortably tap or read. Resetting flex hands the height declaration back. */
  /* Stacked, the field and the button were flush — the dark button growing
     straight out of the bottom of the input with no seam, reading as one
     mis-drawn control rather than two.

     A gap alone does not fix it: the frame and its surface belong to the FORM,
     so any space opened between the two children is just more of the same white
     panel, and the field appears to grow taller rather than to separate. The
     frame therefore moves off the form and onto the input, which is the element
     that actually is a box to type in — leaving a bordered field, a real gap,
     and a solid button as three distinct things. */
  .hero-form { flex-direction: column; max-width: none; gap: 10px; border: 0; background: none; }
  .hero-form input  { flex: 0 0 auto; height: 54px; width: 100%;
                      border: 1px solid var(--line-strong); background: var(--paper); }
  .hero-form button { flex: 0 0 auto; height: 54px; width: 100%; justify-content: center; }
  .hero-input-tip, .hero-form-msg { max-width: none; }

  /* The hero sub is justified upstairs so its right edge runs straight against
     the hero's rule — a real improvement AT its 47ch measure, which needs about
     500px of column. Below roughly 556px of viewport the measure is no longer
     47ch, it is whatever the phone allows (~30ch), and justification stops
     buying an edge and starts buying damage: rivers of white opening mid-line,
     and hyphenation forced into breaks like "employment-de-fense" and
     "attorney-review-able" on nearly every line. Ragged right is correct here;
     `hyphens: manual` keeps the soft hyphens from firing once nothing is being
     stretched to justify them. */
  .hero-sub { text-align: left; hyphens: manual; -webkit-hyphens: manual; }
}

@media (max-width: 720px) {
  .caps-body { grid-template-columns: 1fr; }
  .statement-inner .lead { font-size: 16px; }
  .foot-legal { font-size: 12.5px; }
}

/* ---- capability rail, lying down ----
   A GRID, not the wrapping flex row this used to be. Wrapped flex lines pack
   from the left and every tab keeps its own intrinsic width, so the second row
   starts at a different x than the first and the four labels never line up
   (measured: Exposure began at 144, Action Plan at 157 — a 13px stagger that
   reads as an accident). Equal grid tracks give all four the same origin, so
   the block is uniform however it breaks.

   Column counts are EXPLICIT rather than auto-fit: auto-fit resolves to three
   columns across a stretch of tablet widths, which orphans the fourth tab on a
   row of its own. Four across, then a true 2x2 — never 3+1. The 640px switch is
   where four tracks stop clearing the widest label ("04 Action Plan", 125px of
   content) with margin to spare.

   The outer breakpoint is 720, matching the rule directly above that collapses
   .caps-body to a single column — NOT the 900 the stage file used for the old
   flex version. Between 721 and 900 the body still runs as `168px 1fr`, so the
   rail is a narrow left-hand column; laying four tracks across it gave each tab
   42px. The rail may only lie down once the body has stopped being two. */
@media (max-width: 720px) {
  .caps-rail {
    position: static;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0;
    padding: 0;
    border-left: 0; border-bottom: 0;
    margin-bottom: clamp(24px, 4vw, 34px);
  }
  /* The hairline moves from the rail onto each tab. On one row they join into
     the single rule the rail used to draw; wrapped to 2x2 each row keeps its
     own, so the active underline still has something to sit against. */
  .caps-tab {
    width: auto;
    padding: 12px 6px 12px 12px;
    margin-left: 0; margin-bottom: 0;
    border-left: 0;
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
  }
  .caps-tab.is-active { border-left-color: transparent; border-bottom-color: var(--ink); }
}

@media (max-width: 640px) {
  .caps-rail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---- the hero dial stands down on phones ----
   It is a WIDE instrument: a 700-unit square whose meaning lives in the
   labelled outer arc (§ 510 · OT 33%, § 226.7 · MEAL 31%, ...). Squeezed into
   a phone-width band it resolved to roughly 370x152 — the callouts crop away
   entirely and what survives reads as decorative hatching rather than as data,
   while still costing a WebGL context and a paint. There is no honest way to
   show a square diagram in a narrow horizontal band, so it stands down and the
   hero is copy + form, which is what that column is for. The cut is at 760 so
   iPad portrait (768) still gets the dial. It is aria-hidden and purely
   decorative, so nothing is lost to assistive tech. */
@media (max-width: 760px) {
  .hero-right { display: none; }
}

/* ---- deliverables list on phones ----
   Reclaiming every pixel the row can spare — tighter panel padding, a tighter
   number gap, a hair off the type — buys back about 20px of measure, which is
   enough to pull "Evidence mapped to each allegation" and "Employee and
   pay-period calculations" up onto single lines. Five of the eight then hold
   one line each.

   The other three do NOT, and no amount of tuning will fix that: the longest
   entry is 52 characters and the panel is 337px wide on a 393px phone. Fitting
   it would take roughly 10px type, which is smaller than the numbers beside it
   and not a trade worth making for a set-piece list. So the wrapped ones are
   made to look deliberate instead: text-wrap balance splits them into two even
   lines rather than a full line plus an orphan, and the flex row already keeps
   the runover aligned under the text rather than under the number.

   CORRECTION: text-wrap:balance is gone. Evening the two lines meant the first
   one stopped early ON PURPOSE — "A defensible exposure range" breaking with
   room to spare, when "with visible" would have fitted — which reads as a
   layout fault rather than a nicety. Rows 04 and 07 were the obvious tells.
   Greedy wrapping fills each line and only breaks when it must, so a row that
   runs over looks like it ran over rather than like it gave up early. */
@media (max-width: 560px) {
  .val-doc { padding: 20px; }
  .val-row { gap: 12px; padding: 13px 0; font-size: 15px; }
}

/* ============================================================
   MOBILE COMPOSITION PASS
   These rules have to beat this file's own top-level declarations
   AND v3-stages.css, so they live at the very end where source
   order settles it without a specificity arms race.
   ============================================================ */

/* ---- the hero headline fills its measure ----
   h1 is clamp(43px, 5.5vw, 88px), and 5.5vw does not reach 43px until roughly
   780px of viewport — so on every phone the type sits on the FLOOR of that
   clamp and stops scaling exactly where the column keeps shrinking. Measured at
   488px: the longest line ("Record-backed") ran 284px inside a 432px column,
   leaving a third of the measure standing empty down the right-hand side.

   The line breaks themselves cannot be improved. No two of these words fit on
   one line at any size the column allows, so the wrap is already optimal and
   balance/greedy make no difference to it. The only lever is the type, which
   now scales with the viewport instead of resting on a fixed floor.

   Measured fill of the longest line, before -> after: 320px 108% (it was
   breaking the hyphen inside "Record-backed" to cope) -> 93%, 375px 89% -> 90%,
   393px 84% -> 90%, 488px 66% -> 86%.

   The query runs to 960 rather than 760 deliberately. Handing back to the
   desktop clamp leaves a step in the type size wherever it happens, so it may
   as well happen at 960, where the hero goes from one stacked column to two and
   the whole composition changes anyway — a step there is read as the layout
   changing, whereas a step at 760 would be read as a bug. */
@media (max-width: 960px) {
  .hero h1 { font-size: clamp(31px, 11.6vw, 64px); }
}

/* ---- centre the narrow column ----
   Once every section is a single stacked column, copy that was set left to sit
   against a facing element has nothing left to align to: the section heads were
   centred while the cards and showcase columns underneath them stayed left, so
   each section changed its mind halfway down. Centred throughout on phones.

   The eyebrow's leading rule is dropped where it is centred, matching the
   .eyebrow.center convention the solution head already uses — a centred label
   with a dash hanging off one side reads as lopsided.

   Body copy also picks up a measure and auto margins. Centred text needs a
   short line to stay readable, and without this the blurbs would run the full
   648px of a 720px viewport. */
@media (max-width: 720px) {
  #problem .sec-head,
  #capabilities .sec-head { text-align: center; margin-inline: auto; }
  #problem .sec-head .eyebrow::before,
  #capabilities .sec-head .eyebrow::before { display: none; }
  #problem .sec-head .lead,
  #capabilities .sec-head .lead { margin-inline: auto; }

  .problem-card { text-align: center; }
  .pc-body { max-width: 44ch; margin-inline: auto; }

  .show-col { text-align: center; }
  .show-col .show-blurb { max-width: 44ch; margin-inline: auto; }

  /* text-align inherits, and the graphics are made of text — statute numbers,
     record labels, counters, axis captions. Centring the column therefore
     re-set the INSIDE of every panel as well, which is wrong twice over: those
     are data rows that read left-to-right down a column, and their alignment is
     load-bearing for the grid they sit in. The copy is centred; the instruments
     are handed their left edge back. */
  .show-col .glass-card,
  .caps-viz { text-align: left; }

  /* ---- capability cards: label, graphic, then explanation ----
     The card is laid out graphic-left / copy-right on desktop, which it does
     with `order` — .caps-viz 1, .caps-copy 2. Stacked, that order survives and
     puts the whole graphic ABOVE its own number and heading, so each panel
     arrives unlabelled and the reader meets the picture before being told what
     it is.

     `display: contents` dissolves the .caps-copy wrapper so its three children
     become card items in their own right and can be ordered individually —
     which is what lets the heading go above the graphic while the blurb stays
     below it, without touching the markup. Margins are restated here because
     the desktop rhythm was built around a two-column card. */
  .caps-card { gap: 0; text-align: center; }
  .caps-copy { display: contents; }
  .caps-copy .show-eyebrow { order: 1; margin: 0 0 12px; }
  .caps-copy .show-label   { order: 2; margin: 0 0 22px; }
  .caps-viz                { order: 3; }
  .caps-copy .show-blurb   { order: 4; margin: 22px auto 0; max-width: 44ch; }

  /* ---- the proof terminus follows its own spine ----
     v3-stages.css left-aligns this block with the timeline above it, but the
     chip row's `justify-content: center` is declared at the top level of THIS
     file, which loads later and won — so the block read left-aligned with one
     stubbornly centred row of chips at the bottom of it. */
  .pp-chips { justify-content: flex-start; }
}
