/* ==========================================================================
   neatforms landing — landing.css
   DOC 2 build. Structure: reset → tokens → base type → layout primitives →
   components → utilities. Mobile-first; min-width queries only. No !important.
   ========================================================================== */

/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; }
h1, h2, h3, p, dl, dd, figure, ul, ol { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg, canvas { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
table { border-collapse: collapse; width: 100%; }
summary { cursor: pointer; }

/* ---------- tokens ----------
   The palette is DESIGN.md's, not this file's. Every value below is the app's
   own token under a marketing-side name, so the two surfaces read as one
   product (see .impeccable/surfaces/marketing-pages-home-html.md — the
   marketing/** waivers cover type only, and there is no colour waiver).
   Name here          value      app token in public/styles.css
   ------------------ ---------- ------------------------------ */
:root {
  --ink:     #1E2023;  /* graphite — body text            (--gray-900)  */
  --paper:   #DEDBD2;  /* warm brushed steel — page ground (--surface)  */
  --surface: #FFFFFF;  /* plate face: cards, panels        (--white)    */
  --rule:    #DAD7CE;  /* steel-mid: borders, dividers     (--gray-200) */
  /* One step darker than the app's --gray-600 #6E6B62, and deliberately so:
     the app runs secondary text on white cards, but this surface sets a lot of
     it straight onto the steel ground, where #6E6B62 is only 3.85:1. #5C5A54
     is 4.98:1 on steel and 6.6:1 on white — same warm steel-ink, AA either way. */
  --steel:   #5C5A54;  /* steel-ink: secondary text        (~--gray-600) */
  --signal:  #FF6B00;  /* safety orange — ACTIONS ONLY     (--orange)   */
  /* Safety orange as TEXT on the steel ground is only 2.06:1, so the kicker
     and any other orange lettering use this darkened step (4.95:1). Fills and
     lamps keep the true brand orange above. */
  --signal-ink: #993F00;

  /* The app's plate vocabulary, needed now that marketing draws real plates. */
  --plate-face:   #EFEDE7;  /* nested/section headers       (--surface-card) */
  --hairline:     #C7C4B9;  /* rule inside a plate          (--hairline)     */
  --graphite:     #23262B;  /* dark plate: header, footer   (--navy)         */
  --graphite-2:   #2C2F35;  /* its gradient partner         (--navy-2)       */
  --steel-mute:   #A9A69C;  /* disabled, chevrons           (--gray-400)     */
  --orange-light: #FFF3E8;  /* tinted orange fill           (--orange-light) */
  --orange-lit:   #FF8F3D;  /* orange legible on graphite   (--orange-lit)   */

  /* Risk lamps — app values, never a flat chip without its lit dot.

     The lamps light the GRAPHICS: the dot, the rail, a border, where the bar
     is 3:1 and all three clear it. The -ink steps set the TYPE, where the bar
     is 4.5:1 and green and amber do not — 3.97:1 and 3.37:1 on white (NEAT-144).
     Same split --signal-ink runs above. High needs no ink step: 5.58:1 on white.
     Both inks are step 1 of their ramp in .impeccable/design.json. */
  --risk-low:    #23924F;  --risk-low-bg:    #E4F3E8;  --risk-low-ink:    #175E33;
  --risk-medium: #C9790A;  --risk-medium-bg: #FBF0DF;  --risk-medium-ink: #7A4907;
  --risk-high:   #C7291F;  --risk-high-bg:   #FBEAE8;

  /* Depth, verbatim from the app: a bevel and a contact shadow, never a float. */
  --shadow:      0 1px 0 rgba(255,255,255,0.55) inset, 0 2px 6px rgba(20,18,14,0.16);
  --shadow-lg:   0 16px 34px -10px rgba(10,9,7,0.5);
  --shadow-glow: 0 0 0 3px rgba(255,107,0,0.18), 0 3px 10px -2px rgba(255,107,0,0.65);
  --focus-ring:  0 0 0 3px rgba(255,107,0,0.16);

  /* Milled corners (app scale): plates 6, controls 4, inputs 3. */
  --radius:    6px;
  --radius-md: 4px;
  --radius-sm: 3px;

  /* The how-it-works screen width at desktop. Named because two rules need it:
     the plate itself, and the reserve the step list keeps clear for it. */
  --steps-plate-w: 300px;

  /* Primary fills carry white normal-weight labels, so they sit on the darker
     step: white on --signal is 2.86:1. Owner reviewed #D95800 on-device and
     accepted the residual shortfall against 4.5:1 to stay on the brand orange
     (#C24E00 at 4.79:1 is the value that would pass). Recorded in the surface
     brief — do not silently revert this to --signal. */
  --signal-dark:  #D95800;  /* app --orange-dark */
  --signal-press: #B54900;  /* hover/active on a --signal-dark fill (5.36:1) */

  /* type families — self-hosted; real faces arrive as woff2 (§4.2).
     System fallbacks keep the structural pass legible until then. */
  --display: 'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --body:    'Public Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono:    'IBM Plex Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;

  /* spacing scale (§4.3) — 4/8/12/16/24/32/48/64/96/128 */
  --s-4: 4px;   --s-8: 8px;   --s-12: 12px; --s-16: 16px; --s-24: 24px;
  --s-32: 32px; --s-48: 48px; --s-64: 64px; --s-96: 96px; --s-128: 128px;

  --wrap: 1160px;
  --measure: 68ch;
  --sticky-h: 56px;
}

/* ---------- fonts (§4.2) — subset woff2, self-hosted ----------
   Archivo and Public Sans are variable fonts (Archivo carries the width axis
   §4.2 relies on). Axis ranges instanced to what the page uses to hold the
   <90 KB budget: Archivo wght 400–600 / wdth 85–110, Public Sans wght 400–600. */
@font-face {
  font-family: 'Archivo';
  src: url('/assets/fonts/archivo-var.woff2') format('woff2');
  font-weight: 400 600;
  font-stretch: 85% 110%;
  font-display: swap;
}
@font-face {
  font-family: 'Public Sans';
  src: url('/assets/fonts/publicsans-var.woff2') format('woff2');
  font-weight: 400 600;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('/assets/fonts/plexmono-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

/* ---------- base type (§4.2) ---------- */
/* The canvas takes the FOOTER's ink, not the page's steel.

   With no background here, body's brushed steel propagates to the canvas and
   paints the whole window — including any area past the end of the document.
   FAQ is the shortest page at 1198px, so on any window taller than that a band
   of pale steel appeared under the ink footer and read as the footer breaking
   off. Every page does it; FAQ just runs out first.

   Setting it here stops that propagation: body paints its own box, the canvas
   paints everything beyond. Because this is the footer's own --ink, an underfilled
   window reads as the footer band simply continuing, and bottom overscroll
   rubber-banding gets the same treatment rather than flashing steel.

   Pushing the footer down with a flex body instead would put the slack ABOVE
   the footer, between the console CTA and it, which is the same band in a worse
   place. */
html { background: var(--ink); }

body {
  font-family: var(--body);
  font-weight: 400;
  font-size: clamp(1rem, 0.98rem + 0.2vw, 1.0625rem);   /* body */
  line-height: 1.55;
  color: var(--ink);
  /* Brushed-steel ground, the same 115° hairline over warm steel the app
     runs (public/styles.css body). The page is a plate; panels rest on it. */
  background:
    repeating-linear-gradient(115deg, rgba(255,255,255,0.5) 0px, rgba(255,255,255,0.5) 1px, transparent 1px, transparent 3px),
    var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 { font-family: var(--display); }

/* Weight 600, not 700. The subset is instanced wght 400–600 (see @font-face
   above), so 700 asked for a weight the file does not carry and the browser
   answered with synthetic bold — measured: "Every form" at 100px renders
   554.7px wide at 600, 700 and 800 alike, while 400 and 500 each step. Fake
   bold smears the stems and loses the width axis's crispness. Ask for what the
   font has; the display presence comes from size and the 110% width, which are
   real. Re-instancing the subset to 800 is the alternative and costs KB. */
/* The clamp FLOOR moved 2.05rem -> 3.25rem (34px -> 52px at 390) with the
   console ground, direction E. Only small screens move: the upper bound and
   the vw term are untouched, so the 1440 rendering is identical. 34px reading
   as a document heading rather than a product's name was half of why the mobile
   hero was inert — the reference set (Resend, Linear) runs 52-72px there.

   text-wrap: balance is what makes 52px survivable: unbalanced, "Forms for your
   crews." breaks after "your" and orphans "crews." on its own line. Balanced it
   sets as "Forms for / your crews." */
h1 {
  font-weight: 600;
  font-stretch: 110%;
  font-size: clamp(3.25rem, 1.4rem + 3.1vw, 4.25rem);
  line-height: 1.06;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
h2 {
  font-weight: 600;
  font-stretch: 110%;
  font-size: clamp(1.65rem, 1.2rem + 2.1vw, 2.75rem);
  line-height: 1.12;
  letter-spacing: -0.015em;
}
h3 {
  font-weight: 600;
  font-size: clamp(1.15rem, 1.1rem + 0.25vw, 1.25rem);
  line-height: 1.30;
}

/* One step above h2, for the single section that carries the page's argument.
   Without it every section shouts at exactly the same volume and the page reads
   as a list rather than as a sequence with a peak. Use once per page. */
.h2--major {
  font-size: clamp(2rem, 1.3rem + 3vw, 3.75rem);
  line-height: 1.05;
  letter-spacing: -0.022em;
}

.body-lg {
  font-size: clamp(1.0625rem, 1.02rem + 0.4vw, 1.1875rem);
  line-height: 1.50;
}
.caption { font-size: 0.875rem; line-height: 1.45; }

/* Stencil-bracket kicker — the app's .card-kicker device, marking a section
   the way an engraved plate label marks a module. The brackets come from CSS,
   never from the copy, so the markup stays clean. */
.eyebrow {
  position: relative;
  font-family: var(--mono);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-size: 0.6875rem;
  color: var(--signal-ink);
}
.eyebrow::before { content: "[ "; }
.eyebrow::after  { content: " ]"; }

/* mono metadata (§4.2) */
.mono-meta {
  font-family: var(--mono);
  font-weight: 400;
  font-size: 0.75rem;
  line-height: 1.20;
  text-transform: uppercase;
  letter-spacing: 0.10em;
  color: var(--steel);
}

/* ---------- layout primitives ---------- */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: 16px;
}
@media (min-width: 768px) { .wrap { padding-inline: 24px; } }

/* Mobile floor tightened 40→32 to hold the §5.1 scroll budget; desktop unchanged.
   §4.3 scale values (32/64/96/128) preserved at each end. */
.section        { padding-block: clamp(32px, 7vw, 96px); }
.section--tight { padding-block: clamp(24px, 5vw, 64px); }
.section--major { padding-block: clamp(48px, 9vw, 128px); }

/* Dark bands are the app's graphite plate, not a generic dark section. */
.section--dark { background: var(--graphite); color: #F5F1EC; }
.section--dark .eyebrow { color: var(--orange-lit); }
.section--dark .mono-meta { color: var(--steel-mute); }

/* A light plate sitting inside a dark band is still a LIGHT surface, so the
   .section--dark text rules above must stop at its edge. They did not: the
   booking form's Privacy Policy link inherited --paper (#DEDBD2) on white, about
   1.3:1 and effectively invisible, and [DRAFT] markers inside the panel dropped
   from --steel to --steel-mute. Any new .section--dark descendant rule needs a
   matching reset here.

   The base colour is reset on the panel itself, not on each thing inside it
   (NEAT-189). Listing descendants only guards the ones somebody remembered:
   the booking form's success message — an <h3> that inherits and nothing else —
   rendered #F5F1EC on white, 1.05:1, invisible. It shipped that way because
   every OTHER child of that plate happened to name its own colour, so the plate
   looked covered. Every .panel is background-color:var(--surface), so ink on
   the container is right for all of them and a child added later inherits it.
   The two rules below still earn their place: --paper and --steel-mute are
   named outright further down, so inheritance never reaches them. */
.section--dark .panel { color: var(--ink); }
.section--dark .panel .link-out { color: var(--ink); }
.section--dark .panel .mono-meta { color: var(--steel); }

/* The same class of bug, on the hero's phone screen — and this one is
   functional, not cosmetic. .section--dark sets color:#F5F1EC on the section;
   the mini-FLHA's fields take `color: inherit` on a white --surface fill, so
   with the hero on graphite a visitor typing their job site into it would see
   near-white text on white. The booking form below only escapes this because
   .demo-fields input sets color:var(--ink) outright — the mini-FLHA was written
   for a light ground and never had to.
   Guarded in both directions: the container resets here, and the fields name
   their own colour (see .miniform input, below). tests/visual/hero.test.js
   types into the form and scores each field against its own background. */
.hero.section--dark .phone-plate__screen { color: var(--ink); }

/* /product's coordinator block moved onto the console (2026-08-10) and brought
   three light-ground assumptions with it. --rule (#DAD7CE) is near-white on
   graphite and --steel is ~2.2:1, exactly as documented for the step rows. */
.section--dark .dl > div { border-top-color: rgba(255,255,255,0.13); }
.section--dark .dl > div:last-child { border-bottom-color: rgba(255,255,255,0.13); }
.section--dark .dl dt { color: var(--steel-mute); }

/* .pdf-artifact is the one element allowed to float, and its float shadow is
   tuned for the steel ground — on graphite it is simply invisible. The sheet is
   white and draws its own edge, so what it needs here is contact, not lift:
   the plate bevel deepened enough to register against the console. Deliberately
   NOT a diffuse ambient drop — see DESIGN.md's Contact-Shadow-Not-Float rule.

   :not() excludes the hero's reveal, which sits on the phone plate's brushed
   steel rather than on the console itself and owns its edge outright.

   A LIT RING, NOT A TOP-ONLY HIGHLIGHT. The top-only inset was written when the
   sheet's title bar was navy #1A1A2E, which had enough blue in it to separate
   from the warm console on its own. NEAT-111 moved that bar to graphite-ink
   #1E2023, and --graphite is #23262B — the two are 1.08:1 apart, i.e. the same
   colour. The sheet's whole top edge stopped being an edge, and the bottom
   three quarters carried the object alone.

   This is the same pair, and the same fix, the power-rail seam note below
   already records: "graphite sits five points off the ink footer, and without a
   lit line between them the two read as one". Same 1px lit ring .hero
   .phone-plate uses to hold its own against this ground. */
.section--dark .pdf-artifact:not(.miniform--reveal .pdf-artifact) {
  box-shadow:
    0 0 0 1px rgba(255,255,255,0.16),
    0 1px 0 rgba(255,255,255,0.55) inset,
    0 2px 10px rgba(0,0,0,0.45);
}

/* Power-rail seam: the console's one lit edge and the fourth signature device.
   It also does load-bearing work — graphite (#23262B) sits five points off the
   ink footer (#1E2023), and without a lit line between them the two read as one
   undifferentiated dark mass.

   2px, matching .nav::after. It shipped at 1px and read as a different device
   from the nav's rail rather than the same one at the other end of the panel —
   the console opened on a 2px line and closed on a 1px one. HOME-HERO-CONSOLE.md
   §3 anticipated this exact call: "if the top boundary ever reads underweight,
   take it to 2px to match the nav's rail." Every PLAIN orange divider on the
   site is now one weight and one colour.

   The hazard stripe is deliberately NOT unified with these: 6px at 45 degrees is
   a different device marking a section ENTRANCE, where the seam marks an exit. */
.section--console { box-shadow: inset 0 -2px 0 var(--signal); }

/* The closing CTA centres its text — except on Home, where it does not.

   All four closing CTAs use the same 640px column. On Home that column holds
   the booking form card, which fills it, so the heading left-aligning to the
   card's own edge is right. The other three hold a heading, one line of subhead
   and a small button, none of which reach 640 — so left-aligned text sat at
   x=424 against a page gutter of 340 on FAQ and 140 on Pricing and Product,
   lining up with nothing on the page and reading as a block that had slipped.

   Keyed on data-cta-end, which already marks "the CTA that closes the page"
   (it is what tells the sticky bar to stand down), so this cannot catch the
   hero, which is also a .section--console. */
.section--console[data-cta-end]:not(.final-cta) > .wrap { text-align: center; }

.measure { max-width: var(--measure); }

/* An authored line break that only exists where there is room for it. */
.br-md { display: none; }
@media (min-width: 768px) { .br-md { display: inline; } }

/* The hero's next-section marker jumps here, and the nav is sticky at 56px, so
   the heading needs to clear it. Matches #demo-form's own scroll-margin-top. */
#library { scroll-margin-top: 72px; }

.hairline { border: 0; border-top: 1px solid var(--rule); margin: 0; }

/* ---------- hazard-stripe motif (§4.6) ---------- */
.hazard-stripe {
  height: 6px;
  background: var(--ink);
  background-image: repeating-linear-gradient(
    45deg,
    var(--signal) 0, var(--signal) 8px,
    var(--ink) 8px, var(--ink) 16px
  );
}

/* ---------- buttons (§4.4, §4.5) ---------- */
/* Every button is a switch on a panel: a bevel highlight along the top edge,
   a hairline of contact underneath, and a real mechanical depress on press.
   Same vocabulary as the app's .btn. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 13px 22px;
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: 0.01em;
  cursor: pointer;
  box-shadow: 0 1px 0 rgba(255,255,255,0.25) inset, 0 1px 0 rgba(0,0,0,0.18);
  transition: background-color 100ms linear, box-shadow 100ms linear;
}
/* Mechanical press: the switch depresses rather than just shifting down. */
.btn:active {
  transform: translateY(1px);
  box-shadow: 0 1px 2px rgba(0,0,0,0.25) inset;
}
/* Filled with --signal-dark, not --signal: these labels are white, and white on
   #FF6B00 is 2.86:1. The fill is the darker step so the button still reads as
   the brand orange while clearing the large-text minimum. */
.btn--primary {
  background: var(--signal-dark);
  color: #fff;
  border-color: var(--signal-dark);
  box-shadow: var(--shadow-glow), 0 1px 0 rgba(255,255,255,0.3) inset;
}
.btn--primary:hover { background: var(--signal-press); }
/* The sign-up submit ships disabled until the Terms box is ticked, so this state
   is on screen every time somebody opens the form — it is the FIRST thing they
   see, not an edge case. Steel rather than a faded orange: a dimmed brand fill
   reads as a rendering fault, the same reason .rail-arrow:disabled dims its glyph
   instead of the whole control.

   ⛔ The label is --steel, NOT --steel-mute. Mute is the token for a chevron or a
   placeholder, and at #A9A69C on this ground it is about 1.6:1 — the first draft
   of this rule used it and the button read as an empty grey slab with a smudge on
   it. This label is doing work: it is what tells somebody who has not ticked the
   box yet what the button is going to do. --steel clears 5:1 here.

   Hover and the mechanical press are both suppressed, or a dead button still
   answers the pointer. */
.btn:disabled,
.btn--primary:disabled,
.btn--primary:disabled:hover {
  background: var(--paper);
  color: var(--steel);
  border-color: var(--rule);
  box-shadow: none;
  cursor: not-allowed;
}
.btn:disabled:active { transform: none; box-shadow: none; }
.btn--ghost { background: var(--surface); color: var(--ink); border-color: var(--rule); }
.btn--ghost:hover { border-color: var(--steel); }

.link-out {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  color: var(--ink);
  border-bottom: 1px solid transparent;
}
.link-out:hover { border-bottom-color: var(--signal); }
.section--dark .link-out { color: var(--paper); }

/* The hero's only CTA since the primary button came out, so it has to outrank a
   footnote link two sections down. The SIZE does that job alone.

   It used to also carry a rest-state orange underline, which made it the one
   link on the site drawn as though it were already hovered — every other link
   here, in the nav, and in the footer underlines on interaction. Now it matches
   them: .link-out's transparent 1px border holds the space, and .link-out:hover
   lights it, so nothing shifts on hover. */
.link-out--lead {
  font-size: 1.0625rem;
}

/* ---------- focus (§13) ---------- */
:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
}

/* ---------- skip link (§13) ---------- */
.skip {
  position: absolute;
  left: 8px; top: -60px;
  z-index: 200;
  background: var(--ink); color: #fff;
  padding: 10px 16px; border-radius: 4px;
  transition: top 120ms linear;
}
.skip:focus { top: 8px; }

/* ---------- rail (§5.2) ---------- */
.rail-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 16px;
}
.rail-counter { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.10em; color: var(--steel); }

.rail {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  padding-inline: 16px;
  margin-inline: -16px;             /* bleed to text margin */
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { flex: 0 0 78vw; scroll-snap-align: start; }
/* Snap to the padding edge, not the scroller edge. Without this the first card
   snaps flush to x=0 and the rail's own 16px left padding scrolls out of view,
   so the card sits hard against the viewport while the heading above it is
   inset 16px. */
.rail { scroll-padding-left: 16px; }

@media (min-width: 768px) {
  .rail {
    display: grid;
    grid-template-columns: repeat(var(--rail-cols, 4), 1fr);
    overflow: visible;
    padding-inline: 0;
    margin-inline: 0;
    gap: 16px;
  }
  .rail > * { flex: initial; }
  .rail-counter { display: none; }
}

/* ---------- library rail — the form examples on Home ----------
   Everything here is scoped to .rail--library. The plain .rail is shared with
   /product's how-it-works and modules rails; widening those would be wrong.

   Why this rail keeps scrolling on desktop while the others switch to a grid:
   its cards are photographs of real PDFs, written at 480px with denser
   candidates beside them (RASTER in scripts/build-form-artifacts.mjs). A
   four-up grid draws them near 285px, and the tables and risk badges that make
   them convincing turn to grey mush. Fewer, larger cards and a scroll is the
   only way the artwork does its job. */
/* Arrows appear wherever the visitor has a precise pointer, not at a width.
   Keying them to 768px meant a narrowed laptop window lost them, leaving a
   mouse user to drag a rail — awkward, and there is no swipe to fall back on.
   A touch device gets no arrows at any width because the swipe IS the control,
   and the peeking next card already advertises it. */
@media (any-pointer: fine) {
  .rail-head:has(+ .rail--library) .rail-nav { display: inline-flex; gap: 8px; }
}

/* One continuous expression at every width, deliberately NOT split across the
   768px breakpoint. Two rules meeting at that seam made the card jump from
   420px to 598px as the window narrowed through it — a bigger card in a smaller
   window. 78vw keeps roughly two-and-a-bit cards in view on a wide screen and
   one-plus-a-peek on a phone, and the 480px cap is the width the 1x artwork is
   written at. */
.rail--library > * {
  /* Read it as: never wider than 78% of the viewport (the phone case, one card
     plus a peek), never wider than 480px (the wide case, ~2.3 in view), and
     never past 600px. Whichever is smallest wins, so the value can only fall as
     the window narrows — no seam, no inversion.

     The card images carry this same expression in their `sizes` attribute, and
     the candidate ladder is written to suit it. Change the basis here and you
     must change both, or the browser asks for a width nothing is written at.

     480px was tried at 720px once, to make the body text legible, and the owner
     reported no improvement — because the artwork it enlarged was washed out by
     the builder's downsampler. That is fixed at the source now. Note what this
     cap does NOT do: at 480 a whole 8.5in page puts 9pt type on 7.1 device px,
     which no amount of resolution makes readable. The card is a thumbnail and
     the click is what reads it. */
  flex: 0 0 min(78vw, 480px);
  scroll-snap-align: start;
}

@media (min-width: 768px) {
  .rail--library {
    display: flex;
    /* Bleed the scroller to the section's own text margin so a card can sit
       flush left while the overflow still clips inside the wrap. */
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    /* No inline padding at this width, so the snap edge is the scroller edge. */
    scroll-padding-left: 0;
    padding-inline: 0;
    margin-inline: 0;
    gap: 28px;
    padding-block: 4px 8px;   /* room for the sheet's bevel, top and bottom */
    scrollbar-width: none;
  }
  .rail--library::-webkit-scrollbar { display: none; }
  /* Soften the cut edge so the third card dissolves instead of being sliced,
     which reads as "there is more" rather than as a clipping bug. The mask
     lifts once the rail is scrolled to the end (data-at-end from landing.js),
     so the last card is never left looking faded. */
  .rail--library {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 40px), rgba(0,0,0,0.35) 100%);
            mask-image: linear-gradient(to right, #000 calc(100% - 40px), rgba(0,0,0,0.35) 100%);
  }
  .rail--library[data-at-end] {
    -webkit-mask-image: none;
            mask-image: none;
  }
  /* This rail is the one place the counter earns its keep on desktop, because
     this is the one rail that still scrolls there. The head precedes the rail,
     so it is reachable only by :has() on the next sibling. */
  .rail-head:has(+ .rail--library) .rail-counter { display: inline; }
}

/* Milled square buttons in the same language as .btn: bevel, mechanical press,
   no scale. They are enhancement — see landing.js. */
.rail-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  color: var(--ink);
  background: var(--plate-face);
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  box-shadow: 0 1px 0 rgba(255,255,255,.45) inset, 0 1px 0 rgba(0,0,0,.14);
  cursor: pointer;
  transition: background-color 120ms linear, box-shadow 120ms linear;
}
.rail-arrow svg { width: 20px; height: 20px; display: block; }
.rail-arrow:hover:not(:disabled) { background: var(--surface); }
.rail-arrow:active:not(:disabled) {
  box-shadow: 0 1px 2px rgba(0,0,0,.18) inset;
  transform: translateY(1px);
}
/* Dim the glyph, not the whole control. Fading the button to 0.35 on steel left
   it reading as a rendering artifact rather than a disabled button. */
.rail-arrow:disabled {
  cursor: default;
  color: var(--steel-mute);
  background: transparent;
  box-shadow: none;
}

/* ---------- form card — a PDF, not a card holding a PDF ----------
   No background, no border and no radius of its own, and deliberately NOT
   .panel. The sheet below carries the bevel, so the thing that looks physical is
   the page rather than a card wrapped around it. */
/* It is a <button> now (it opens the record), so the browser's button chrome has
   to come off before the card can look like a sheet again. */
.form-card {
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-width: 0;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.form-card__meta { display: block; }
/* THE RULE IS AN OVERLAY, NOT A BORDER, AND THAT IS LOAD-BEARING.
   A 1px border makes the image's content box 478px inside a 480px card, so the
   480px artifact gets fractionally downscaled to 478 on every card, at every
   density, forever. Measured at DPR 1: it cost 29% of the card's edge acuity
   (0.099 -> 0.070) and was the largest contributor left once the rasteriser was
   fixed. Drawing the rule in ::after keeps it off the layout, so the image lands
   1:1 at exactly 480px.

   It must be ::after and not an inset box-shadow: an inset shadow paints below
   child content, so the image covers it completely and the card loses its edge
   with nothing to show for it. That was tried here first and the rule vanished —
   the measurements still passed, because none of them look at the border. */
.form-card__sheet {
  position: relative;
  display: block;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--shadow);
  transition: box-shadow 120ms linear;
}
.form-card__sheet::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid var(--rule);
  border-radius: inherit;
  pointer-events: none;
}
.form-card__sheet img {
  width: 100%;
  height: auto;
  display: block;
}
/* Lift on the shadow only — no transform, no scale, no glow. */
.form-card:hover .form-card__sheet,
.form-card:focus-visible .form-card__sheet {
  box-shadow: 0 1px 0 rgba(255,255,255,.55) inset, 0 6px 18px rgba(20,18,14,.22);
}
.form-card:focus-visible { outline: none; }
.form-card:focus-visible .form-card__sheet {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
}
.form-card__meta .mono-meta,
.form-card__meta .caption { display: block; }
.form-card__meta .caption { margin-top: 6px; }

/* ---------- form library lightbox ----------
   The record a card opens. There is no other overlay on this surface, so this is
   the whole pattern: scrim, one plate, a close control, the document, and the
   download. Modelled on the app's own .file-preview (public/admin.js) — same
   scroll lock, same focus return, same Escape and Tab trap, so the two surfaces
   behave identically. */
.form-lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(12px, 3vw, 40px);
  /* Graphite, not black: the scrim is the console dimming, not a generic modal. */
  background: rgba(20, 22, 26, 0.82);
}
.form-lightbox[hidden] { display: none; }

/* 818px: 816px of body between two 1px borders. A Letter page is 8.5in = 816
   CSS px at 96dpi, so the page image lands at life size and the browser barely
   scales it. build-form-artifacts.mjs writes the pages at 816 and 1632 to
   match — the two numbers move together.
   Not exact everywhere: where the platform draws a classic scrollbar rather
   than an overlay one, the body loses ~15px and the page renders about 2%
   under. Downscaling by that is invisible; sizing UP to cover the scrollbar
   would blur every page on the platforms that do not have one. */
.form-lightbox__plate {
  display: flex;
  flex-direction: column;
  width: min(100%, 818px);
  max-height: 100%;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

/* A page cut off mid-sentence at a hard edge reads as a rendering bug, not as a
   document that continues. On a 900px-tall desktop the body shows about 62% of
   ONE page of a two-page record — 1456px of it is below the fold, behind an
   overlay scrollbar that stays invisible until you have already scrolled.

   Same answer .rail--library uses for the same problem: soften the cut so it
   reads as "there is more". A wrapper rather than a background on the body
   itself, because the pages are opaque white and would paint straight over a
   background gradient. */
.form-lightbox__scroll {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.form-lightbox__scroll::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 44px;
  background: linear-gradient(to top, var(--paper), rgba(222, 219, 210, 0));
  pointer-events: none;
  transition: opacity 140ms linear;
}
/* Nothing left to scroll to: landing.js sets this once the body is at its end,
   exactly as the rail lifts its mask at [data-at-end]. */
.form-lightbox__scroll[data-at-end]::after { opacity: 0; }

.form-lightbox__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--rule);
}
.form-lightbox__title {
  font-size: clamp(1.125rem, 1rem + 0.7vw, 1.5rem);
  margin-top: 4px;
}
.form-lightbox__close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--ink);
  background: var(--plate-face);
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  box-shadow: 0 1px 0 rgba(255,255,255,.45) inset, 0 1px 0 rgba(0,0,0,.14);
  cursor: pointer;
}
.form-lightbox__close svg { width: 20px; height: 20px; display: block; }
.form-lightbox__close:hover { background: var(--surface); }
.form-lightbox__close:active { box-shadow: 0 1px 2px rgba(0,0,0,.18) inset; transform: translateY(1px); }

/* The document itself. --paper behind it so a page's own white edge reads as a
   sheet resting on the plate rather than bleeding into it. */
.form-lightbox__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  background: var(--paper);

}
/* The document is the pages as pixels, stacked and scrolled — no <iframe>, at
   any width. See the note above the markup in marketing/pages/home.html.
   The rule between them is the only thing separating one sheet from the next,
   so it is structure here rather than decoration; the last page keeps it too,
   as the end of the paper against the plate.

   img AND canvas, styled identically on purpose: landing.js paints the prebuilt
   image first, then swaps in a canvas rendered at the real device pixel ratio.
   The swap must be invisible — same width, same flow, same separating rule — or
   the plate would visibly reflow a moment after opening. */
.form-lightbox__body img,
.form-lightbox__body canvas {
  display: block;
  width: 100%;
  height: auto;
  border-bottom: 1px solid var(--rule);
}

.form-lightbox__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 12px 20px;
  border-top: 1px solid var(--rule);
}
.form-lightbox__note { color: var(--steel); margin: 0; }

/* On a phone the plate is the whole screen, and the chrome was taking 232px of
   820px — head 96 plus foot 136 — leaving the document 586px to live in. Most
   of the foot was the note wrapping to two lines at .caption's 14px while the
   48px button sat beside it.
   The note is provenance, not instruction: it earns a line when there is room
   and gets out of the way when there is not. The button keeps its 48px target
   untouched — that is a tap-target floor, not a style. */
@media (max-width: 560px) {
  .form-lightbox__foot {
    flex-wrap: nowrap;
    gap: 12px;
    padding: 10px 16px;
  }
  .form-lightbox__note {
    font-size: 0.8125rem;
    line-height: 1.35;
    min-width: 0;   /* let it shrink rather than forcing the button to wrap */
  }
  /* The note yields; the button does not. Without this the note's text keeps
     its intrinsic width and squeezes the button until "Download PDF" wraps to
     two lines and the control grows to 81px — taller than the row it saved. */
  .form-lightbox__foot .btn { flex: none; white-space: nowrap; }
}

/* Below ~380px there is no width left for the note to be squeezed into: at
   320px it compresses to 86px, wraps to four lines and the foot grows BACK to
   109px, which is the opposite of the point. It is provenance rather than
   instruction — the document above is self-evidently a neatforms record — so
   the smallest screens get the document and the download instead. */
@media (max-width: 380px) {
  .form-lightbox__note { display: none; }
  .form-lightbox__foot { justify-content: flex-end; }
  .form-lightbox__head { padding: 12px 16px; }
}

body.is-locked { overflow: hidden; }

/* ---------- riveted plate — REMOVED, everywhere ----------
   Owner decision, 2026-08-10 for marketing and 2026-08-19 for the system. The
   four corner bolt-heads used to sit on .panel, .tiers__plate, .picker and
   .miniform here, copied from public/styles.css so both surfaces milled the same
   corner. There are now no rivets on either surface: the app's went with
   NEAT-169 on 2026-08-24, four of them having gone on drawing for five days after
   the rule was written off as dead.

   What holds these containers together instead: the milled 6px radius, the 1px
   --rule border, and the plate bevel + contact shadow in --shadow. Do not
   reintroduce them on either surface. The comments further down this file that
   still speak of "the corner rivets" and "the shared .rivet-plate rule above"
   describe a rule that no longer exists; they are kept only because the layout
   workarounds they explain are still doing their job. */

/* ---------- rail cards / generic panels ----------
   A plate resting on the steel ground: milled 6px corner, bevel highlight and
   contact shadow. The v3 build shipped these square, borderless and flat,
   which is what made the page read as a different product. */
.panel {
  background-color: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.tag {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--steel);
  background: var(--plate-face);
  border: 1px solid var(--rule);
  padding: 3px 8px;
  border-radius: var(--radius-sm);
}

/* ---------- missing-asset placeholder (§10.4) ---------- */
.asset-missing {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  border: 1px dashed var(--rule);
  color: var(--steel);
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 24px 16px;
  background: var(--surface);
}

/* NEEDS-CONTENT marker (build-time only; not a design element) */
.needs-content {
  border-left: 3px solid var(--signal);
  background: #fff;
  padding: 12px 14px;
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.5;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--steel);
}
.section--dark .needs-content { background: rgba(255,255,255,.04); color: #A7AEBC; }

/* ==========================================================================
   components
   ========================================================================== */

/* ---------- nav (§6.0) ----------
   Graphite plate with a one-pixel safety-orange power-rail seam beneath it —
   DESIGN.md's Navigation section, verbatim, including "present on every screen".

   Marketing shipped this white until 2026-08-10. The seam is defined as a LIT
   EDGE under a dark panel, so a white bar left the device doing nothing: the
   only place it read on Home was inside .phone-plate__header, i.e. on the
   picture of the product and never on the page itself.

   White is not otherwise wrong on this surface — --surface is "plate face:
   cards, panels", and the PDF sheets, the mini-FLHA and the booking panel all
   earn it. The nav was the one place white was chrome rather than a plate.

   The seam is --signal here, not --signal-dark: #FF6B00 is what DESIGN.md names
   for the power rail, and #D95800 only ever existed to hold up against white.

   It is always on — the seam is the brand, not a scroll affordance — and
   .is-scrolled only deepens the drop shadow. */
/* The seam is an ::after rather than the box-shadow it used to be, because the
   power-on arrival wipes it in from the left and a shadow cannot be wiped.
   .nav is position:sticky, which is a containing block, so the absolute child
   lands on the bar's bottom edge exactly where the shadow drew. The drop shadow
   .is-scrolled adds is still a shadow — only the lit line moved. */
.nav {
  position: sticky;
  top: 0;
  z-index: 100;
  height: var(--sticky-h);
  background: var(--graphite);
  transition: box-shadow 120ms linear;
}
.nav::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 2px;
  background: var(--signal);
  transform-origin: left;
}
.nav.is-scrolled { box-shadow: 0 4px 12px rgba(0,0,0,0.35); }
/* EDGE TO EDGE, deliberately not the 1160px cap the .wrap class on this element
   otherwise applies. The bar's graphite and its power-rail seam already span the
   viewport; capping the CONTENTS at 1160 left the lockup floating 380px inboard
   on a 1920 screen, so a full-width bar framed a narrow one. Owner call,
   2026-08-17.

   The element keeps .wrap for its padding and box-sizing — only the cap is
   lifted. This rule follows .wrap in source order, so it wins at equal
   specificity; moving it above .wrap silently restores the 1160. Nothing below
   1160px viewport changes, because the cap was never reached there. */
.nav__inner {
  max-width: none;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
/* The 24px page gutter is judged against a 1160 column and reads tight against a
   1920 screen edge. One step up the 8px scale (§4.3) once there is room. */
@media (min-width: 1280px) {
  .nav__inner { padding-inline: var(--s-32); }
}
/* The stock lockup is #1C1C28 on transparent and would vanish on graphite.
   Swapping the rendered source keeps one <img> and one alt in the shared
   partial rather than forking nav.html per page. */
/* 32px, not the 22px this shipped with, so the wordmark sits at the nav links'
   letter-height. The box height flatters the lockup: its 250x64 viewBox carries
   roughly a third of its height as whitespace, so at 22px only ~15px was ink and
   the wordmark drew about 60% of the 17px links beside it — the mark read as a
   favicon that had wandered into the bar. 32px is the largest step that still
   leaves 12px of air top and bottom in the 56px sticky bar; 36px matched nothing
   better and crowded it. Sized by eye against the rendered links rather than by
   arithmetic, because the SVG's own <text> falls back to a system face whose cap
   height is not knowable from the file. */
.nav__logo { height: 32px; content: url("/brand/neatforms-lockup-light.svg"); }
.nav__links { display: none; }
.nav__mobile { display: flex; align-items: center; gap: var(--s-8); }

/* Burger = the #nav-toggle checkbox itself, drawn with three background bars.
   Styling the control rather than a <label> keeps it focusable and Space-
   operable with no JS and no aria plumbing. */
.nav__burger {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 44px;
  height: 44px;
  margin: 0;
  border: 1px solid rgba(255,255,255,0.22);
  border-radius: 4px;
  background-color: transparent;
  background-image:
    linear-gradient(#F5F1EC, #F5F1EC),
    linear-gradient(#F5F1EC, #F5F1EC),
    linear-gradient(#F5F1EC, #F5F1EC);
  background-repeat: no-repeat;
  background-size: 20px 2px;
  background-position: 11px 14px, 11px 21px, 11px 28px;
  cursor: pointer;
}

/* Drawer: closed by default, opened by the checkbox via :has(). Hidden
   outright at desktop so its links never double up with .nav__links. */
.nav__drawer { display: none; }
.nav:has(#nav-toggle:checked) .nav__drawer {
  display: grid;
  gap: var(--s-4);
  padding: var(--s-8) var(--s-16) var(--s-16);
  border-top: 1px solid rgba(255,255,255,0.13);
  background: var(--graphite);
  font-weight: 500;
}
.nav__drawer a {
  padding: 12px 0;
  border-bottom: 1px solid rgba(255,255,255,0.13);
  min-height: 44px;
  color: #F5F1EC;
}
.nav__drawer a:last-child { border-bottom: 0; }

/* The seam draws at the CLOSED bar's bottom edge, which the open drawer then
   paints straight over — the brand's one lit edge disappeared exactly when
   someone opened the menu. Hand it to the drawer for the duration. Now that the
   seam is .nav::after this hides the pseudo-element rather than a shadow; the
   drawer's own lit edge is still a shadow, since nothing animates it. */
.nav:has(#nav-toggle:checked)::after { display: none; }
.nav:has(#nav-toggle:checked) .nav__drawer { box-shadow: 0 2px 0 var(--signal); }

@media (min-width: 768px) {
  .nav__links {
    display: flex;
    align-items: center;
    gap: 24px;
    font-weight: 500;
  }
  .nav__links a { color: #F5F1EC; }
  /* A lit edge on hover, matching the seam above — not an underline. */
  .nav__links a:not(.btn):hover { border-bottom: 1px solid var(--signal); }
  .nav__mobile { display: none; }
  .nav:has(#nav-toggle:checked) .nav__drawer { display: none; }
}

/* ---------- hero (§6.1) ----------
   Centred on a phone, left-aligned from 768px up. A single left-aligned column
   of eyebrow / headline / subhead / link, all on the same margin and the same
   width as the plate below it, is the shape of a document — which is exactly
   how the mobile hero read. Centring turns the first screen into a title card.
   Desktop keeps the left edge, where the copy has a plate beside it to align
   to and centring would have nothing to centre against. */
/* ---------- the console ground (direction E) ----------
   markdowns/HOME-HERO-CONSOLE.md, approved 2026-08-13. The band's graphite and
   its seam come from .section--dark / .section--console in the layout
   primitives; what follows is the work those two classes do not do.

   position/overflow are for the rake (.hero::after, below): it is a pass of
   light wider than the band and has to be clipped to it. The stacking rule that
   follows keeps every child above it — without it the rake paints over the copy
   at its peak. */
.hero {
  position: relative;
  overflow: hidden;
}
.hero > * { position: relative; z-index: 1; }

/* Load-bearing, and the single most important declaration on this page. The
   plate fills #23262B and --graphite IS #23262B, so on this ground the plate
   has no edge at all and the hero's one bright object dissolves into the stage
   holding it. --graphite-2 is DESIGN.md's "gradient partner to Graphite Plate
   on dark screens", so the plate sits one step above the console, and the 1px
   lit edge is .steps__plate's own recorded device — the same fix, for the same
   reason, already made once on #how. */
.hero .phone-plate,
.hero .phone-plate__header { background: var(--graphite-2); }
/* The honesty note under the window is already --graphite-2, so lifting the
   plate body to that value flattened all three into one tone and the footer
   stopped being a footer — it survived on its 1px top border alone. It takes
   the darker step here, which also puts it in the same relationship to the
   plate that the plate has to the console: each surface one step off the one
   holding it. */
.hero .phone-plate__foot { background: var(--graphite); }
.hero .phone-plate {
  box-shadow:
    0 0 0 1px rgba(255,255,255,0.16),
    0 30px 66px -24px rgba(0,0,0,0.85);
}

/* Trap 2. .hero__next is drawn for the steel ground: --signal-ink (#993F00) is
   2.2:1 on graphite, and the arrow inherits it. The LABEL survives on its own
   because .section--dark .eyebrow reroutes it to --orange-lit — a partial
   rescue that hides the arrow being broken, which is exactly what a screenshot
   waves through. .hero__next-rule keeps --steel-mute and reads better on
   graphite than it did on steel; left alone. */
.hero.section--dark .hero__next { color: var(--orange-lit); }

.hero__copy { text-align: center; }
.hero__eyebrow { margin-bottom: 12px; }
.hero h1 { margin-block: 0 16px; }
/* A step below the inherited #F5F1EC. Without it the headline and the subhead
   sit at the same tone and the pair reads as one block of light text. */
.hero__sub { color: var(--paper); max-width: 34ch; margin-bottom: 24px; margin-inline: auto; }
.hero__cta { display: flex; flex-direction: column; gap: 12px; align-items: center; }
.hero__secondary { margin-top: 4px; }
.hero__proof { margin-top: 24px; }
.hero__form { margin-top: 32px; }

/* ---------- next-section marker ----------
   Desktop only. On a phone the mini-FLHA plate already runs off the bottom of
   the first screen, which is the cue; a marker there would sit a screen and a
   half down and spend scroll budget saying nothing. */
.hero__next { display: none; }

@media (min-width: 768px) {
  /* Size the hero to the screen so its bottom edge IS the fold, and put the
     next-section marker on that edge. Before this, the hero stopped ~96px short
     of its own boundary and the following section spent another 96px on padding
     before drawing anything, so at 1440x900 the last 91px of the first screen
     was bare ground and the library kicker landed at 905 — five pixels out of
     sight. Nothing was cropped, so nothing implied a continuation.

     svh, not vh: vh measures the viewport with mobile/tablet browser chrome
     retracted, which would push the marker under the address bar.

     The padding drop from clamp(32px,7vw,96px) is what makes short laptops
     work. At 1280x720 there are 664px below the nav; the copy column is ~583px,
     so at 7vw the hero came to 775px, min-height never bound, and the marker
     fell BELOW the fold. Re-measure 1280x720 before restoring a larger value. */
  .hero {
    display: flex;
    flex-direction: column;
    min-height: calc(100svh - var(--sticky-h));
    padding-block: clamp(32px, 4vw, 64px);
  }
  .hero__inner {
    display: grid;
    grid-template-columns: 6fr 5fr;
    gap: 48px;
    align-items: center;
    flex: 1;
  }
  /* Back to the left edge: from here up the copy has the plate beside it, and a
     centred column would align to nothing. */
  .hero__copy { text-align: left; }
  .hero__cta { flex-direction: row; flex-wrap: wrap; align-items: center; }
  .hero__sub { max-width: var(--measure); margin-inline: 0; }
  /* The plate is a fixed window, not a column that grows with the form. It is
     capped near the copy's own height so the hero reads as one composition and
     the whole thing stays inside the first viewport; the form scrolls inside
     it, which is how it behaves on a phone anyway. align-items:center rather
     than stretch, so a short copy column does not drag the window taller. */
  .hero__form {
    margin-top: 0;
    min-height: 0;
  }
  .hero__form .phone-plate {
    /* Track the copy column rather than the viewport, so the pair reads as one
       block; the ceiling keeps it inside the first screen on short laptops. */
    height: 100%;
    /* 58vh/500 -> 63vh/600 with the console ground: the bottom quarter of the
       desktop hero was bare ground, and on graphite an empty quarter reads as a
       missing block rather than as breathing room. The vh term is the safety —
       a flat 600 pushes .hero__next below the fold at 1366x768. Measured across
       all eight viewports in the fold table (HOME-HERO-CONSOLE.md §8): the
       marker keeps exactly today's headroom at every one. min(66vh,620px) also
       cleared but left 13px at 1024x600 against today's 26px, which is how a
       marker slips under the fold on somebody else's next change.
       Do not raise this without re-running that table — tests/visual/hero.test.js
       is its executable form. */
    max-height: min(63vh, 600px);
    width: 100%;
    /* Pin to the column's right edge. The plate is capped at 380px but the
       column is wider, so centring it leaves the plate floating ~100px inboard
       of the content edge — and since the plate is the dominant object in this
       half, the eye reads its edge as the page's. The nav CTA directly above
       then looks pushed out to the right. Flush right lines the plate up with
       the CTA and with every .wrap edge below it. */
    margin-inline: auto 0;
  }
  /* The record is a letter page, taller in proportion than the form window.
     Holding the form's 500px ceiling squeezes it to an unreadable strip.

     height:100% has to go with it: the hero grid centres its items, so the
     column is shrink-to-fit and 100% resolves to the plate's own content
     height — which pins it at the form's size and stops max-height ever
     applying. Sizing to content lets the taller cap do its job. */
  .hero__form .phone-plate:has(.miniform--reveal:not([hidden])) {
    height: auto;
    max-height: min(78vh, 680px);
  }

  /* margin-top:auto pins it to the hero's bottom edge, which the min-height
     above has parked at the fold. A leader line rather than a floating arrow:
     the 1px rule fades up out of nothing, the way a callout is drawn on an
     equipment diagram, and the label is the .eyebrow device so the brackets
     come from CSS and it reads as a panel readout. The .wrap on the element
     lands it on the same left edge as the H1 and the proof strip. */
  .hero__next {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    margin-top: auto;
    padding-top: 24px;
    color: var(--signal-ink);
    text-decoration: none;
  }
  /* The rule descends INTO the label, so it has to be its own row above it —
     as a flex sibling in a row it rendered as a 1px tick beside the arrow and
     the marker read as orphaned in the gap above it.
     --steel-mute, not --rule: #DAD7CE sits four points off the --paper ground
     and was invisible on screen. */
  .hero__next-rule {
    flex: none;
    width: 1px;
    height: 40px;
    margin-left: 5px;
    background: linear-gradient(to bottom, transparent, var(--steel-mute));
  }
  .hero__next-label { display: flex; align-items: center; gap: 10px; }
  /* A transition, never a keyframe, for the HOVER nudge specifically: a
     keyframed bounce would be killed outright by the reduced-motion block at
     the foot of this file, while a transition degrades to a correct static
     state. (The file no longer carries zero @keyframes — the power-on arrival
     below is a deliberate exception, and it is guarded the other way round, by
     prefers-reduced-motion: no-preference. This rule's reasoning is unchanged.) */
  .hero__next-arrow {
    font-size: 0.875rem;
    line-height: 1;
    transition: transform 120ms linear;
  }
  .hero__next:hover .hero__next-arrow { transform: translateY(2px); }
  .hero__next:hover .eyebrow { border-bottom: 1px solid var(--signal); }
}

/* Short desktop windows. The copy column is ~583px of text that cannot shrink,
   so on a screen under ~730px tall the hero overflows its own min-height and the
   marker slides below the fold — exactly where the cue is needed most. Tightening
   the hero's own gaps buys back ~75px and carries it down to roughly 660px tall.
   Below that the fold cuts the hero's content instead, which is itself a cue.
   Measured, not estimated: see the viewport table in the surface brief. */
@media (min-width: 768px) and (max-height: 760px) {
  .hero { padding-block: clamp(20px, 2.5vw, 32px); }
  .hero h1 { margin-bottom: 12px; }
  .hero__sub { margin-bottom: 16px; }
  .hero__proof { margin-top: 16px; }
  .hero__next { padding-top: 16px; }
}

/* ---------- the power-on arrival (direction E, §5d) ----------
   THIS BREAKS THIS FILE'S ZERO-@KEYFRAMES INVARIANT ON PURPOSE. That invariant
   (asserted at .hero__next-arrow above and at the steps' lit dot) was written
   against an INFINITELY PULSING GLOW — a decorative loop that reduced-motion
   users would lose entirely, leaving them a dot that never lights. This is a
   one-shot arrival: it runs once, on load, never on scroll, and every element
   ends at its resting state. Recorded in the surface brief so the next agent
   does not "fix" it back.

   Guarded by no-preference, NOT by the blanket !important block at the foot of
   this file. That block kills animation-duration but not animation-delay, so a
   stagger built on it would flash missing content for the length of every
   delay — 900ms of invisible headline for the users who asked for less motion,
   which is the opposite of the intent. Outside this query nothing below exists:
   no opacity:0, no transform, no animation. The static page IS the
   reduced-motion state.

   Authored, not indiscriminate: one ~900ms sequence for the page's first
   screen. The banned-compositions list forbids fade-in-on-scroll, which is a
   different thing — this never observes scroll.

   The keyframes sit at file level and only their APPLICATION is scoped to
   .hero, so opting another page into the arrival later is adding a selector. */
@keyframes seam-wipe   { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes rise-12     { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes rise-8      { from { opacity: 0; transform: translateY(8px); }  to { opacity: 1; transform: none; } }
@keyframes fade-in     { from { opacity: 0; } to { opacity: 1; } }
/* The kicker's own text, not its brackets: transparent -> lit, so the [ ] set
   first and the label sprays in between them. */
@keyframes ink-in      { from { color: transparent; } to { color: var(--orange-lit); } }
/* The plate LANDS: it rises the furthest and its shadow deepens from flat to
   full as it arrives, so the movement reads as an object settling onto the
   console rather than a card fading up. */
@keyframes plate-land {
  from {
    opacity: 0;
    transform: translateY(16px);
    box-shadow: 0 0 0 1px rgba(255,255,255,0.16), 0 6px 14px -8px rgba(0,0,0,0.85);
  }
  to {
    opacity: 1;
    transform: none;
    box-shadow: 0 0 0 1px rgba(255,255,255,0.16), 0 30px 66px -24px rgba(0,0,0,0.85);
  }
}
/* One specular pass across the panel — a rake of light, the cue that a lit
   surface has just been switched on. Once, never repeating: `forwards` would
   park the sheet on the panel, so it ends by running off the far edge.

   Opacity does ALL the fading, and the gradient's own stops do all the shaping.
   The first build angled the gradient inside the moving box and skewed the box
   as well; the two compounded into a hard-edged wedge that sat across the
   headline like a slab, with a visible boundary — a specular pass has no edge
   at all. So: the sheet is wider than it travels is fast, the gradient runs
   along the box (not across it at an angle), and the skew alone supplies the
   115deg rake of the ground's grain. */
@keyframes rake {
  from { opacity: 0; transform: translateX(-90%) skewX(-25deg); }
  30%  { opacity: 1; }
  70%  { opacity: 1; }
  to   { opacity: 0; transform: translateX(190%) skewX(-25deg); }
}

@media (prefers-reduced-motion: no-preference) {
  .nav::after {
    animation: seam-wipe 380ms cubic-bezier(.16,1,.3,1) both;
  }

  .hero__eyebrow { animation: ink-in 300ms linear 230ms both; }
  /* The brackets are their own animation and their own colour. Without the
     explicit --orange-lit they would follow the parent's animated `color` and
     set transparent along with the label, which loses the whole point of the
     first two rows: the stencil frame appears, then the word is sprayed into
     it. */
  .hero__eyebrow::before,
  .hero__eyebrow::after {
    color: var(--orange-lit);
    animation: fade-in 260ms linear 110ms both;
  }

  .hero h1        { animation: rise-12 520ms cubic-bezier(.16,1,.3,1) 200ms both; }
  .hero__sub      { animation: rise-8  480ms cubic-bezier(.16,1,.3,1) 300ms both; }
  .hero__cta      { animation: rise-8  480ms cubic-bezier(.16,1,.3,1) 380ms both; }
  .hero .phone-plate { animation: plate-land 620ms cubic-bezier(.16,1,.3,1) 420ms both; }
  .hero__next     { animation: fade-in 400ms linear 820ms both; }
  /* The lamp lights last — the panel is up, then it reports a state. */
  .hero .risk-badge::before { animation: fade-in 240ms linear 900ms both; }

  /* The rake is a child of the band, so it sits UNDER .hero > * (z-index 1) and
     washes the ground and the plate's surround without ever dimming the copy.
     46% wide so it reads as a pass of light rather than a wipe of the whole
     panel. */
  .hero::after {
    content: "";
    position: absolute;
    /* Overhang top and bottom so the skewed sheet's own corners never enter the
       band — a skewed rectangle has points, and a point crossing the copy is
       the hard edge this is built to avoid. */
    top: -30%;
    bottom: -30%;
    left: 0;
    width: 46%;
    z-index: 0;
    pointer-events: none;
    /* 90deg — along the box, not across it. The peak is broad and both tails
       reach fully transparent well inside the sheet, so what crosses the panel
       is a soft band of light with no boundary anywhere in it. */
    background: linear-gradient(
      90deg,
      rgba(255,255,255,0) 0%,
      rgba(255,255,255,0.04) 30%,
      rgba(255,255,255,0.15) 50%,
      rgba(255,255,255,0.04) 70%,
      rgba(255,255,255,0) 100%
    );
    animation: rake 1250ms cubic-bezier(.3,.5,.35,1) 300ms both;
  }
}

/* ---------- phone plate (§7.1) ----------
   The frame that makes the hero read as "the app, running" rather than a
   form that borrows a few app-flavored details. Same graphite fill and
   orange power-rail seam as the real .app-header in public/styles.css —
   copied at the marketing surface's own token names, not approximated. */
/* A device-sized window, not a column that grows with its contents. The whole
   FLHA is far taller than the hero copy beside it, so the plate is bounded and
   the screen scrolls inside it — which is also how the form behaves on a real
   phone. Desktop matches the height to the copy column (see the 768px rule). */
.phone-plate {
  /* One inset for the whole window. The header and footer take it literally;
     the screen divides it by --screen-scale because zoom shrinks its padding.
     Change it here and all three edges move together. */
  --plate-inset: 17px;
  display: flex;
  flex-direction: column;
  max-width: 380px;
  max-height: 68vh;
  margin-inline: auto;
  border-radius: 20px;
  overflow: hidden;
  background: #23262B;
  box-shadow: 0 24px 60px -20px rgba(20,24,43,0.45), 0 2px 0 rgba(0,0,0,0.2);
}
/* The header and footer are not zoomed, so they take the screen's inset as a
   literal value: the form card sits 17px in from the plate edge, and the brand
   mark and the note line up with it. Left unmatched, the mark sits inboard of
   the card below it and the whole window reads as shifted. */
.phone-plate__header {
  /* Flex, not block: an inline-flex child sits on a line box, so the mark
     inherits the font's descender space and rides a few px high in the bar.
     Making the header itself the flex container centres the mark against the
     bar rather than against a line of text. */
  display: flex;
  align-items: center;
  background: #23262B;
  padding: 14px var(--plate-inset);
  /* Power-rail seam: the panel's one lit edge, present on every real app screen. */
  box-shadow: 0 2px 0 var(--signal-dark);
}
.phone-plate__brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 0.9rem;
  color: #F5F1EC;
}
.phone-plate__brand img { display: block; }
.phone-plate__header,
.phone-plate__foot { flex: none; }
.phone-plate__screen {
  flex: 1;
  min-height: 0;                    /* let the flex child actually shrink */
  /* Containing block for .miniform-gate, which covers exactly this box. */
  position: relative;
  /* NOT a scroll container until the visitor presses Start (NEAT-141). The
     form is ~1680px of content in a ~578px window, so an always-live screen
     put an internally scrollable region across 53% of the first viewport at
     390x844 — and overscroll-behavior below deliberately stops that scroll
     chaining on to the page, so the opening swipe reached nothing. Gated, the
     page scrolls normally under the visitor's first gesture; once they have
     chosen the form, .is-live hands the screen its scroll back.

     overscroll-behavior stays: it is correct once the form is in use, and
     while gated there is no overflow for it to act on. */
  overflow: hidden;
  overscroll-behavior: contain;     /* don't chain the scroll to the page */
  -webkit-overflow-scrolling: touch;
  /* Draw the form at phone width, then scale the whole screen down to fit the
     window. Shrinking the type instead would break the 16px floor that stops
     iOS zooming a field on focus, so the form keeps its real metrics and the
     window shows more of it at once.

     zoom shrinks this box's own padding along with everything else in it, but
     not the plate around it. So the inset is divided back out by the scale:
     --plate-inset / 0.82 is drawn larger, then scaled down to land the form's
     edges on the same 17px the header and footer use. Without that, the form
     card sits inboard of the brand mark and the window reads as misaligned.

     The bottom padding is roomy on purpose, so the form card scrolls clear of
     the window edge rather than being sliced flush against it, which reads as
     a clipping bug rather than a screen you can scroll. */
  --screen-scale: 0.82;
  zoom: var(--screen-scale);
  padding:
    calc(16px / var(--screen-scale))
    calc(var(--plate-inset) / var(--screen-scale))
    calc(28px / var(--screen-scale));
  /* Brushed-steel surface: the app's own diagonal hairline texture on its
     warm-steel background, not the marketing surface's flat paper. */
  background:
    repeating-linear-gradient(115deg, rgba(255,255,255,0.5) 0px, rgba(255,255,255,0.5) 1px, transparent 1px, transparent 3px),
    #DEDBD2;
}
.phone-plate__screen.is-live { overflow-y: auto; }

/* ---------- start gate (NEAT-141) ----------
   A scrim over the screen with one control on it. The form stays visible
   behind it on purpose — the hero's whole claim is that this is the product
   running, not a picture of it, and a gate that hid the form would spend that
   proof to solve a scroll problem. The scrim is light enough to read the form
   through and dark enough that nothing behind it reads as pressable. */
.miniform-gate {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Blur first, scrim second. A scrim heavy enough to separate the layers on
     its own (0.45 was the first attempt) turns the brushed steel grey and the
     white fields mid-grey, which spends the hero's whole claim — that this is
     the product running — to solve a scroll bug. Blur says "behind, and not
     yours yet" while leaving the form's colour and shape intact, so the scrim
     can stay light. */
  background: rgba(35, 38, 43, 0.26);
  /* 1px, not the 2.5px this started at. At 2.5 the form behind went to mush —
     which turns "the product, running" back into a picture of a form, the
     exact thing the plate exists to disprove. Enough to read as a layer, not
     enough to stop the visitor reading the fields. The button's own shadow
     below carries most of the separation. */
  backdrop-filter: blur(1px);
  -webkit-backdrop-filter: blur(1px);
}
/* Without blur the scrim is the only thing holding the layers apart, so it has
   to carry the separation alone. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .miniform-gate { background: rgba(35, 38, 43, 0.52); }
}
.miniform-gate[hidden] { display: none; }

/* The screen is drawn at phone width and scaled down by --screen-scale, which
   would take .btn's 48px minimum to 39px and its label to 13.9px. Same trick
   the screen's own padding uses above: draw it larger, let zoom bring it back
   to the intended size rather than to 82% of it. */
.miniform-gate__btn {
  min-height: calc(48px / var(--screen-scale));
  padding: calc(13px / var(--screen-scale)) calc(30px / var(--screen-scale));
  font-size: calc(1.0625rem / var(--screen-scale));
  border-radius: calc(var(--radius-md) / var(--screen-scale));
  /* The button lands wherever the screen's centre falls, which is over a field
     as often as not. Elevation is what makes that read as a control on top of
     the form rather than one tangled into it — without it the button and the
     task card's select looked like a layout fault. */
  box-shadow:
    var(--shadow-glow),
    0 10px 24px -6px rgba(0, 0, 0, 0.55),
    0 1px 0 rgba(255, 255, 255, 0.3) inset;
}

/* A soft fade at the bottom of the screen: the standing cue that there is more
   below, without a scrollbar sitting on the brushed steel. */
.phone-plate__screen::after {
  content: "";
  position: sticky;
  bottom: calc(-28px / var(--screen-scale));
  display: block;
  height: calc(28px / var(--screen-scale));
  /* Pull back out over the screen's own padding so the fade spans the full
     window width rather than stopping at the form card's edges. */
  margin: 0 calc(-1 * var(--plate-inset) / var(--screen-scale)) calc(-28px / var(--screen-scale));
  background: linear-gradient(to top, rgba(222,219,210,0.95), rgba(222,219,210,0));
  pointer-events: none;
}

/* Honesty note under the window: this replica is real but trimmed, and the
   visitor should not infer the product's form is this short. */
.phone-plate__foot {
  padding: 10px var(--plate-inset);
  background: var(--graphite-2);
  border-top: 1px solid rgba(255,255,255,0.10);
  color: #A9A69C;
  font-family: var(--mono);
  font-size: 0.625rem;
  line-height: 1.45;
  letter-spacing: 0.04em;
}

/* ---------- hero mini-form ----------
   A working replica of the shipping FLHA (public/forms.js, public/tasks.js,
   public/app-form-core.js), not a picture of one. The rivets come from the
   shared .rivet-plate rule above.

   No card of its own (NEAT-142). In the app this component IS the top-level
   card on the brushed ground, so it carries a white fill, a rule and a radius.
   Lifted into .phone-plate, the plate already supplies that frame and the
   screen already supplies the ground, so keeping the card stacked three
   grounds inside one another — which impeccable's craft-floor Refuse list
   bans outright, and which the reveal state was already fixed for once.

   The form's own padding goes with it: .phone-plate__screen carries the 17px
   plate inset (divided back out by --screen-scale above), so a second 20px
   here double-inset the fields against a mark that lines up with the plate.

   What the white fill was NOT carrying: the fields keep their own
   background: var(--surface) below, and the labels clear AA on the brushed
   ground on their own (--steel is 4.99:1 on #DEDBD2, --signal-ink 5.21:1). */
.miniform {
  padding: 0;
}
.miniform__field { margin-bottom: 16px; }
/* Field labels match the app: plain sentence case, not mono shouting. The app
   reserves mono uppercase for plate labels (kickers, badges, table heads) —
   the Mono-Means-Machine Rule. */
.miniform label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--steel);
  letter-spacing: 0.2px;
  margin-bottom: 6px;
}
.miniform .req { color: var(--risk-high); }
.miniform input[type="text"],
.miniform input[type="tel"],
.miniform select,
.miniform textarea {
  width: 100%;
  min-height: 48px;
  padding: 12px;
  font-size: 16px;                 /* prevent iOS zoom (§5.5) */
  font-family: inherit;
  border: 1.5px solid var(--rule);
  border-radius: 3px;
  background: var(--surface);
  /* --ink, not inherit. These sit on a white fill, and the hero band now sets a
     light `color` for its graphite ground — inheriting it put typed text at
     1.12:1 on white, which is a field the visitor cannot read what they typed
     into. The second half of the guard above; both are deliberate. */
  color: var(--ink);
  transition: border-color 120ms linear, box-shadow 120ms linear;
}
.miniform input[type="text"]:focus,
.miniform input[type="tel"]:focus,
.miniform select:focus,
.miniform textarea:focus {
  outline: none;
  border-color: var(--signal);
  box-shadow: var(--focus-ring);
}
.miniform textarea { min-height: 0; line-height: 1.45; resize: vertical; }
.miniform select { appearance: none; -webkit-appearance: none; padding-right: 36px;
  background-image: linear-gradient(45deg, transparent 50%, var(--steel) 50%),
                    linear-gradient(135deg, var(--steel) 50%, transparent 50%);
  background-size: 6px 6px, 6px 6px;
  background-position: calc(100% - 20px) 21px, calc(100% - 14px) 21px;
  background-repeat: no-repeat;
}

.miniform__title {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--ink);
  margin-bottom: 16px;
}

/* Stencil-bracket kicker — same "[ LABEL ]" device as the app's
   .extra-form-section-title, marking each section like an engraved plate. */
.miniform__section {
  position: relative;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--signal-ink);
  padding-top: 18px;
  margin-bottom: 10px;
}
.miniform__section::before { content: "[ "; }
.miniform__section::after  { content: " ]"; }

/* Task card — collapsible, with a risk-coloured rail down its left edge.
   Mirrors .task-row in public/styles.css. No rivets: see the .rivet-plate
   rule above for why the device stops at the form's own corners. */
.task-row {
  background-color: var(--surface);
  border: 1px solid var(--rule);
  border-left: 4px solid var(--rule);
  border-radius: var(--radius);
  margin-bottom: 10px;
  overflow: hidden;
}
.task-row.risk-rail-Low    { border-left-color: var(--risk-low); }
.task-row.risk-rail-Medium { border-left-color: var(--risk-medium); }
.task-row.risk-rail-High   { border-left-color: var(--risk-high); }

.task-row__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 11px 13px;
  background: var(--plate-face);
  cursor: pointer;
  user-select: none;
}
.task-row__head:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; }
.task-row__label { font-family: var(--mono); font-size: 0.65rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--steel); }
.task-row__right { display: flex; align-items: center; gap: 8px; }
.task-chevron { width: 16px; height: 16px; flex: none; color: var(--steel-mute); transition: transform 150ms ease; }
.task-row:not(.collapsed) .task-chevron { transform: rotate(180deg); }

/* Collapsed state swaps the fields for a two-line summary. */
.task-summary { display: none; flex-direction: column; gap: 5px; padding: 2px 13px 13px; }
.task-row.collapsed .task-summary { display: flex; }
.task-row.collapsed .task-fields  { display: none; }
.task-summary__name { font-size: 0.8125rem; font-weight: 600; color: var(--ink); }
.task-summary__hazard {
  font-size: 0.72rem;
  line-height: 1.5;
  color: var(--steel);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.task-fields { display: flex; flex-direction: column; gap: 12px; padding: 13px; }

.remove-task-btn {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: 0;
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  color: var(--risk-high);
  font-family: inherit;
  font-size: 0.75rem;
  font-weight: 700;
  cursor: pointer;
}
.remove-task-btn svg { width: 13px; height: 13px; }
.remove-task-btn:hover { background: var(--risk-high-bg); }

/* Secondary "+ Add" switch — white plate, orange 2px edge, same as the app. */
.btn-add {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin-top: 12px;
  padding: 10px 16px;
  background: var(--surface);
  /* Label takes the darkened ink step (6.85:1); the 2px edge stays on the
     brand orange, where it is a non-text element and only needs 3:1. */
  color: var(--signal-ink);
  border: 2px solid var(--signal-dark);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 700;
  cursor: pointer;
  transition: background-color 120ms linear;
}
.btn-add:hover { background: var(--orange-light); }

/* Risk badge — the app's lit-dot device: colour is never a flat fill alone,
   every level pairs with a glowing dot (DESIGN.md's Lit-Not-Flat Rule). */
.risk-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: var(--radius-sm);
}
.risk-badge::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 4px currentColor;
  flex: none;
}
/* Label on the ink step, dot on the lamp (NEAT-144). The ::before above draws
   from currentColor, so recolouring the label alone would drag the lamp down
   with it and quietly put out the light this component is named for — each
   level hands the dot its true hue back explicitly. */
.risk-badge.Low    { color: var(--risk-low-ink);    background: var(--risk-low-bg); }
.risk-badge.Medium { color: var(--risk-medium-ink); background: var(--risk-medium-bg); }
.risk-badge.High   { color: var(--risk-high);       background: var(--risk-high-bg); }

.risk-badge.Low::before    { background: var(--risk-low);    box-shadow: 0 0 4px var(--risk-low); }
.risk-badge.Medium::before { background: var(--risk-medium); box-shadow: 0 0 4px var(--risk-medium); }

/* The risk select recolours to match its level, as it does in the app. Text,
   so it takes the ink step too. */
.risk-select.Low    { color: var(--risk-low-ink);    font-weight: 600; }
.risk-select.Medium { color: var(--risk-medium-ink); font-weight: 600; }
.risk-select.High   { color: var(--risk-high);       font-weight: 600; }

/* PPE ---------------------------------------------------------------- */
.ppe-group-label {
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--steel);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 10px;
}
.ppe-group-label--additional { margin-top: 18px; }
.ppe-group-label .optional { color: var(--steel-mute); font-weight: 400; font-size: 0.8rem; text-transform: none; letter-spacing: 0; }
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; }

/* PPE toggle — the app's rocker-switch chip: a real track-and-knob device,
   not a tinted pill. Every item is switchable, including the standard four,
   which ship pre-thrown because they are the floor. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 48px;
  padding: 8px 14px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  background: #F2F1ED;
  color: var(--steel);
  font-family: inherit;
  font-weight: 600;
  font-size: 0.8rem;
  cursor: pointer;
  box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset;
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}
.chip::before {
  content: "";
  flex: none;
  width: 26px;
  height: 15px;
  border-radius: 8px;
  background-color: var(--steel-mute);
  background-image: radial-gradient(circle 5.5px at 7px 7.5px, #fff 97%, transparent 100%);
  box-shadow: 0 1px 2px rgba(0,0,0,0.25) inset;
  transition: background-color 150ms ease, background-position 150ms ease;
}
.chip:hover { border-color: var(--graphite); color: var(--ink); }
.chip[aria-pressed="true"] {
  background: var(--graphite);
  color: #fff;
  border-color: var(--graphite);
}
.chip[aria-pressed="true"]::before {
  background-color: var(--signal);
  background-position: 12px 0;
  box-shadow: 0 0 4px var(--signal), 0 1px 2px rgba(0,0,0,0.25) inset;
}
.ppe-other-wrap { margin-top: 10px; }

/* Team acknowledgment — numbered crew rows, the FLHA's actual sign-off.
   There is no signature canvas in the product, so there is none here. */
.section-note { font-size: 0.85rem; color: var(--steel); margin-bottom: 16px; line-height: 1.6; }
.worker-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.worker-row input { flex: 1; }
.worker-num {
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #F2F1ED;
  color: var(--steel);
  font-size: 0.7rem;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.remove-worker-btn {
  flex: none;
  width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  color: var(--risk-high);
  cursor: pointer;
}
.remove-worker-btn svg { width: 14px; height: 14px; }
.remove-worker-btn:hover { background: var(--risk-high-bg); }

/* ---------- demo booking form ----------
   The page's one job, so it gets a plate of its own rather than sitting
   directly on the steel with bare inputs. Fields match the app's. */
.demo-plate { padding: 24px 20px; }
.demo-fields { display: grid; gap: 16px; }
.demo-fields label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--steel);
  letter-spacing: 0.2px;
  margin-bottom: 6px;
}
.demo-fields .req { color: var(--risk-high); }
.demo-fields .optional { color: var(--steel-mute); font-weight: 400; font-size: 0.8rem; }
.demo-fields input {
  width: 100%;
  min-height: 48px;
  padding: 12px 14px;
  font-family: inherit;
  font-size: 16px;                 /* ≥16px keeps iOS from zooming on focus */
  color: var(--ink);
  background: var(--surface);
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-sm);
  transition: border-color 120ms linear, box-shadow 120ms linear;
}
.demo-fields input:focus {
  outline: none;
  border-color: var(--signal);
  box-shadow: var(--focus-ring);
}
/* Selects match the inputs above rather than falling back to the OS control,
   which on the steel ground reads as a hole in the plate. appearance:none plus
   an inline chevron, the same treatment the mini-FLHA's selects get. */
.demo-fields select {
  width: 100%;
  min-height: 48px;
  padding: 12px 36px 12px 14px;
  font-family: inherit;
  font-size: 16px;
  color: var(--ink);
  background-color: var(--surface);
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-sm);
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B6B63' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 18px 18px;
  transition: border-color 120ms linear, box-shadow 120ms linear;
}
.demo-fields select:focus {
  outline: none;
  border-color: var(--signal);
  box-shadow: var(--focus-ring);
}
/* An unchosen required select must not look answered. Province is required and
   has no sensible default, so its placeholder sits in the muted ink the labels
   use until the visitor picks something. */
.demo-fields select:invalid { color: var(--steel-mute); }

/* Two columns once there is room. The sign-up form has seven fields where the
   demo form has four; stacked at 720px wide it becomes a column of inputs long
   enough that the submit button leaves the screen. */
@media (min-width: 640px) {
  .demo-fields--two { grid-template-columns: 1fr 1fr; }
  .demo-fields--two .demo-field--wide { grid-column: 1 / -1; }
}

/* ---------- web address field ----------
   The domain is fixed and shown, not typed. Without the suffix in view people
   type "waymarc.neatforms.ca" or "https://waymarc" into the box, and the rules
   then reject them for punctuation they were never meant to supply. */
.slug-input {
  display: flex;
  align-items: stretch;
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--surface);
  transition: border-color 120ms linear, box-shadow 120ms linear;
}
.slug-input:focus-within { border-color: var(--signal); box-shadow: var(--focus-ring); }
/* The border lives on the wrapper, so the input inside carries none of its own
   and must not draw its own focus ring either. */
.demo-fields .slug-input input {
  border: 0;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  background: transparent;
  min-width: 0;
}
.demo-fields .slug-input input:focus { box-shadow: none; border-color: transparent; }
.slug-input__suffix {
  display: flex;
  align-items: center;
  padding: 0 12px;
  border-left: 1px solid var(--rule);
  background: var(--paper);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--steel);
  white-space: nowrap;
}
/* A taken address shows on the FIELD, not only in the line beneath it. Without
   this the message was red and the input it referred to looked untouched, so the
   eye went to whichever field happened to hold the focus ring. */
.slug-input.is-taken { border-color: var(--risk-high); }

/* Reserves its line so the field does not jump when the answer arrives. */
.slug-status {
  margin-top: 6px;
  min-height: 1.2em;
  font-size: 0.8125rem;
  /* Normal weight at rest: this line starts out as a hint ("this is where your
     crew signs in"), and at 600 a hint reads as a verdict. Weight arrives with
     an actual answer. */
  color: var(--steel);
}
.slug-status.is-free  { color: var(--risk-low-ink); font-weight: 600; }
.slug-status.is-taken { color: var(--risk-high);    font-weight: 600; }

/* ---------- the Terms tick ----------
   Clickwrap on the sign-up form. Bordered and padded so it reads as a thing to
   act on rather than as the small print it sits next to: the caption below it is
   a notice nobody has to do anything about, and the two must not look alike.

   ⛔ WHITE, NOT --paper, and that is a correction rather than a preference. The
   first version filled this with --paper — and so does .btn:disabled directly
   beneath it, which is the state this button ships in. Two --paper slabs with
   --rule borders, stacked 20px apart, read as one grey block with a line through
   it, and the submit stopped looking like a button at all. On --surface this row
   reads as the last field in the form, which is what it is, and leaves the
   disabled button as the only grey thing on the card.

   The whole label is the hit area (the <label> wraps the input), so a thumb on a
   phone does not have to find a 22px box. That is also why the padding is on the
   label and not on the span. */
.signup-terms {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  min-height: 48px;
  margin-top: 20px;
  padding: 12px 14px;
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: 0.875rem;
  line-height: 1.45;
  cursor: pointer;
}
/* Sized and coloured like the pricing picker's boxes, which are the only other
   checkboxes on the marketing site. margin-top nudges the box onto the first
   line's optical centre rather than its box top. */
.signup-terms input[type="checkbox"] {
  width: 22px;
  height: 22px;
  margin-top: 1px;
  accent-color: var(--signal);
  flex: none;
  cursor: pointer;
}
.signup-terms:focus-within { border-color: var(--signal); box-shadow: var(--focus-ring); }

/* Two of these now sit together — Terms, then Privacy Policy. The 20px above is
   the gap that separates the pair from the last field, and repeating it between
   them would read as two unrelated questions rather than one consent step. 8px
   keeps them a group while leaving each its own 48px hit area. */
.signup-terms + .signup-terms { margin-top: 8px; }

/* ---------- the total block's two CTAs ----------
   The trial is primary and the invoice path sits under it. One CTA here meant
   the trial was offered nowhere on the page that sells the product. */
.total__act {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  flex: 0 1 auto;
}
.total__act .caption { text-align: center; color: var(--steel); }

/* Below 768 the two CTAs sit SIDE BY SIDE (owner call, 2026-08-28). Stacked,
   they were two 130px buttons down the right of a 356px plate with a line of
   caption wedged between them — a narrow column of controls with the width to
   spare beside it.

   Grid rather than a row of flex children, because "No card for the trial." is
   a sibling BETWEEN the two buttons in the DOM and has to stay there: it is the
   trial button's caption, and moving it in source to suit the layout would read
   it out after the wrong control. The buttons take the top row, the caption
   spans underneath, and the markup order is untouched.

   The quote tier hides the trial button and the caption with [hidden], so the
   paid button would sit in a 1fr column half the plate wide. :has() puts it
   back to full width; where :has() is unsupported it lands at half width and
   still reads correctly, which is why this is not doing the hiding itself. */
@media (max-width: 767px) {
  .total__act {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px 10px;
    width: 100%;
  }
  /* Under the TRIAL button, not spanning both. Stacked, this line captioned the
     button directly above it; spanning a side-by-side pair it sits under "Sign
     up" as well, and that button does take a card. Sizing it to the first column
     keeps it attached to the only path it is true of. */
  .total__act .caption { grid-column: 1; order: 1; text-align: center; }
  .total__act:has([data-cta-trial][hidden]) { grid-template-columns: 1fr; }
}

.demo-error {
  margin-top: 16px;
  padding: 11px 14px;
  background: var(--risk-high-bg);
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  font-weight: 600;
  color: #7A1813;
}

/* ---------- support form (NEAT-225) ----------
   /support is built from the sign-up form's parts. These are the pieces it needs
   that no earlier form had: a textarea, a hint line, a two-line face on the kind
   switch, and the row of picked images.

   ⛔ Sits ABOVE the [hidden] utility at the foot of this file, and has to. The
   page hides its fields, its error line and its thank-you panels with [hidden];
   a display rule here would beat that rule only if it came later in the file. */
.demo-fields textarea {
  display: block;
  width: 100%;
  min-height: 144px;
  padding: 12px 14px;
  font-family: inherit;
  font-size: 16px;                 /* ≥16px keeps iOS from zooming on focus */
  line-height: 1.5;
  color: var(--ink);
  background: var(--surface);
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-sm);
  resize: vertical;
  transition: border-color 120ms linear, box-shadow 120ms linear;
}
.demo-fields textarea:focus {
  outline: none;
  border-color: var(--signal);
  box-shadow: var(--focus-ring);
}

/* Styled as a field label, though it is a span: the file input it names is
   [hidden] behind a button, and a <label> for it would make the text itself open
   the picker. */
.demo-fields .support-images__label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--steel);
  letter-spacing: 0.2px;
  margin-bottom: 6px;
}

/* A line of guidance under a label or under its input. --steel, not
   --steel-mute: this is read, and mute is ~2:1 on the white plate. */
.field-hint {
  margin: -2px 0 8px;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--steel);
}
.demo-fields input + .field-hint { margin: 6px 0 0; }

/* The kind switch names each option and says what it is for, so a person can
   pick without guessing which one "I can't sign in" belongs to. */
.switch__face--stack {
  flex-direction: column;
  gap: 2px;
  text-align: center;
}
.switch__sub {
  font-size: 0.8125rem;
  font-weight: 400;
  color: var(--steel);
}
.switch__opt input:checked + .switch__face .switch__sub { color: inherit; opacity: 0.85; }
.support-fields { margin-top: 24px; }
/* ⛔ A grid item's min-width is its content by default, so one long filename in
   the image row ("Screenshot 2026-09-13 at 10.41.jpg") widened the whole column
   past a 390px screen and pushed every input and the Remove buttons off the right
   edge. A screenshot caught it. Zero lets the name's ellipsis do its job. */
.support-fields .demo-fields > * { min-width: 0; }

/* Picked images as rows, not tiles. A row carries the file name, so a person can
   tell two screenshots apart, and it gives the remove control a full 48px target
   instead of a corner button on a thumbnail. Rules between rows rather than a
   bordered box: this list sits inside the plate that already frames the form, and
   a box here would be a card in a card. */
.support-images:not(:empty) {
  margin: 4px 0 12px;
  border-top: 1px solid var(--hairline);
}
.support-images li {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 64px;
  padding: 8px 0;
  border-bottom: 1px solid var(--hairline);
}
.support-images img {
  width: 48px;
  height: 48px;
  flex: none;
  object-fit: cover;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
}
.support-images__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.875rem;
}
.support-images__remove {
  min-height: 48px;
  padding: 0 12px;
  background: none;
  border: 0;
  font-weight: 600;
  color: var(--risk-high);
  cursor: pointer;
}
.support-images__remove:hover { text-decoration: underline; }

/* The reference in the thank-you panel. Mono so a 0 and an O cannot be confused
   when somebody reads it back to us, at the size of the sentence around it. */
.support-ref {
  font-family: var(--mono);
  letter-spacing: 0.04em;
  color: var(--ink);
}

/* ---------- proof line (§6.2) ----------
   Unused. §6.2's standalone stat band is omitted (no countable numbers yet), and
   the hero capability strip that briefly used .hero__proof was removed by the
   owner on 2026-08-10. Kept because it is the shape §6.2 specifies if numbers
   ever exist — do not repurpose it without checking that brief. */
.proof {
  border-block: 1px solid var(--rule);
}
.proof__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  align-items: baseline;
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  color: var(--steel);
  text-transform: uppercase;
}

/* ---------- definition list (§6.6) ---------- */
.dl { display: grid; }
.dl > div {
  display: grid;
  grid-template-columns: 1fr;
  gap: 4px;
  padding-block: 16px;
  border-top: 1px solid var(--rule);
}
.dl > div:last-child { border-bottom: 1px solid var(--rule); }
.dl dt { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--steel); }
.dl dd { margin: 0; }
@media (min-width: 768px) {
  .dl > div { grid-template-columns: 200px 1fr; gap: 24px; align-items: baseline; }
}

/* ---------- coordinator artifact (§6.6) — only element allowed to float (§4.4) ---------- */
.pdf-artifact {
  border-radius: 2px;
  box-shadow: 0 1px 2px rgba(20,24,43,.08), 0 12px 32px rgba(20,24,43,.10);
}

/* ---------- hero reveal: the finished record ----------
   After submit, the plate's screen shows the PDF that the product's own
   generatePDF() just produced, in the browser's PDF viewer.

   The screen is built for the form: it scrolls, and it carries a deep bottom
   padding so the last field clears the window edge. The record wants the
   opposite — one page-shaped object plus its buttons, all visible at once, no
   scrolling. So while the reveal is up the screen stops being a scroll
   container and becomes a flex column, and the roomy padding is cut back. */
.phone-plate__screen:has(.miniform--reveal:not([hidden])) {
  display: flex;
  /* Explicit: flex defaults to row, and a row item sizes to its own content
     rather than the container — which grew the reveal 41px past the screen's
     content box and clipped the download button off the plate's right edge. */
  flex-direction: column;
  overflow: hidden;
  /* The form is drawn at phone width and scaled down to fit. The record is a
     letter page and already fits, so scaling it again only makes it unreadable.
     With zoom off, the insets are taken literally rather than divided out. */
  zoom: 1;
  /* min-height:0 lets this flex child of the plate actually shrink to the
     window; without it the screen keeps its content height and the last 17px
     of the reveal sit below the fold behind a scrollbar. */
  min-height: 0;
  padding: var(--plate-inset);
}
.miniform--reveal {
  display: flex;
  flex-direction: column;
  gap: 12px;
  flex: 1;
  min-width: 0;
  min-height: 0;
}
/* An IMAGE of the record, not an <iframe> framing the PDF. The frame rendered
   blank in headless Chromium and showed only the page's top-left corner on a
   real phone; see hero-demo.js's showReveal() for the full reasoning and the
   lightbox's 2026-08-12 ruling. Do not put the frame back.

   Height-first sizing, unchanged and still load-bearing: giving this a width
   plus an aspect-ratio makes it claim a full page's height and overflow the
   window, which is what forced an inner scrollbar through the record. It takes
   the height left over after the action row, derives its width from the letter
   ratio, and centres — so the page is whole, unscrolled, and never wider than
   the plate.

   ONE edge, one colour. This carried three treatments at once: a near-white
   --rule border, a white inset highlight along the top only, and a hard drop
   shadow below only — so the top edge dissolved into the white page while the
   bottom was sharp, in two different colour families. Now a single 2px
   --hairline (the app's own inside-a-plate rule colour) all the way round, with
   one soft symmetric contact shadow. */
.miniform--reveal .pdf-artifact {
  flex: 1;
  min-height: 0;
  aspect-ratio: 8.5 / 11;
  width: auto;
  max-width: 100%;
  margin-inline: auto;
  border: 2px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  display: block;
  /* An <img> needs telling what an iframe did for free. */
  object-fit: contain;
  box-shadow: 0 2px 12px rgba(0,0,0,0.35);
}
/* width:100% + min-width:0 rather than relying on stretch: as a column-flex
   child this row sized to its max-content width (387px) inside a 346px screen,
   pushing the download button past the plate's right edge and clipping it. */
.reveal-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  min-width: 0;
}
/* Icon-only, so it is squared off rather than inheriting the text button's
   horizontal padding. aria-label carries the name. */
.reveal-dl {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  width: 44px;
  height: 44px;
  flex: none;
}
.reveal-dl svg { display: block; }
/* Sits under the button row, aligned with Start over above it.

   --ink, and this is the .section--dark trap for the third time. This link
   inherits `.section--dark .link-out { color: var(--paper) }` — #DEDBD2 — and
   the phone screen it sits on IS #DEDBD2 brushed steel, so it rendered at
   exactly 1:1. Invisible, in both reveal branches, on the light surface inside
   a dark band that the standing rule is written about: any .section--dark
   descendant rule needs a matching reset where a light plate begins. The
   screen's container guard sets --ink for the form's fields; this sits outside
   the form, so it names its own. */
.reveal-more { align-self: flex-start; }
/* Two class names, because `.section--dark .link-out` is (0,2,0) and a bare
   `.reveal-more` is (0,1,0) — the first attempt at this fix lost the
   specificity contest and rendered identically invisible. Sits with the other
   .section--dark resets in spirit; kept here so it is next to the element it
   protects. */
.link-out.reveal-more { color: var(--ink); }
@media (min-width: 768px) {
  .coordinator__inner { display: grid; grid-template-columns: 7fr 5fr; gap: 48px; align-items: start; }
  /* The PDF column runs ~840px tall against a ~320px spec list, so the right
     column used to empty out for the bottom two-thirds — the same "empty half
     section reads as a missing block" fault recorded for .steps__list, and far
     louder now that this band is graphite. The list travels with the sheet
     instead: whichever part of the record you are looking at, the line that
     explains it is beside it. Distributing the four rows over the full height
     (the .steps__list fix) was the other option and was wrong here — 225px
     between two-line rows reads as a broken list, not a spread one. */
  .coordinator__inner .dl {
    position: sticky;
    top: calc(var(--sticky-h) + 24px);
  }
}

/* ---------- audit pairs / table (§6.8) ---------- */
.audit-pairs > div {
  padding-block: 16px;
  border-top: 1px solid var(--rule);
}
.audit-pairs > div:last-child { border-bottom: 1px solid var(--rule); }
.audit-pairs dt { font-weight: 600; margin-bottom: 4px; }
.audit-pairs dd { margin: 0; color: var(--steel); }
/* Several record types answering to one destination — stacked, not comma-run,
   so an auditor can scan for the one they came for. */
.audit-list { display: grid; gap: var(--s-4); }

.audit-table { display: none; }
@media (min-width: 768px) {
  .audit-pairs { display: none; }
  .audit-table { display: table; }
  .audit-table th, .audit-table td {
    text-align: left;
    padding: 14px 16px;
    border-top: 1px solid var(--rule);
    vertical-align: top;
  }
  /* Table heads are plate labels: mono, uppercase, bold, on the lighter
     plate face — the same readout language the admin console uses. */
  .audit-table th {
    font-family: var(--mono);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--steel);
    font-weight: 700;
    background: var(--plate-face);
    border-bottom: 1px solid var(--hairline);
  }
}

/* ---------- pricing tiers (§6.9, owner worker-count model) ---------- */
/* ---------- billing switch (§6.9) ----------
   Radios, not buttons: it is one choice out of two, which is what a radio group
   already means. The input stays in the tree (focusable, arrow-key operable) and
   only its box is hidden — the visible face is the sibling span. */
.switch {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8);
  margin: 0;
  padding: 0;
  border: 0;
}
/* A <legend> is laid out against its fieldset's border box, which notches it
   into the plate edge and collides with the corner rivets. Promoting it to a
   normal block takes it out of that special layout so it sits cleanly above. */
.switch__legend {
  display: block;
  float: none;
  width: 100%;
  padding: 0;
  margin-bottom: var(--s-12);
  font-weight: 600;
}
/* Full width until there is room for both faces on one line — side by side at
   390px the "2 months free" chip wrapped and left the two options uneven. */
.switch__opt { flex: 1 1 100%; }
@media (min-width: 560px) { .switch__opt { flex: 1 1 0; } }
/* Visually hidden but still focusable and still in the a11y tree — zero-size or
   display:none would drop it out of the tab order and out of the switch. */
.switch__opt input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
.switch__face {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-8);
  min-height: 48px;
  padding: 10px 16px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 4px;
  font-weight: 600;
  cursor: pointer;
}
.switch__opt input:checked + .switch__face { border-color: var(--ink); background: var(--ink); color: var(--paper); }
.switch__opt input:focus-visible + .switch__face { outline: 2px solid var(--signal); outline-offset: 2px; }
.switch__save {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  padding: 2px 6px;
  background: rgba(20, 24, 43, .08);
}
.switch__opt input:checked + .switch__face .switch__save { background: rgba(255, 255, 255, .16); }

/* The fieldset is a transparent grouping element only — it carries the legend
   but no chrome, because a <legend> laid against a bordered fieldset notches
   the plate edge and lands on the corner rivets. The plate is drawn by
   .tiers__plate inside it. */
.tiers {
  border: 0;
  background: none;
  box-shadow: none;
  margin: 0;
  padding: 0;
  min-inline-size: 0;
}
.tiers__plate {
  border: 1px solid var(--rule);
  background-color: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.tiers__plate .tier:first-of-type { border-top: 0; }
.tier {
  display: flex;
  align-items: baseline;
  gap: 16px;
  padding: 16px;
  min-height: 56px;
  border-top: 1px solid var(--rule);
  cursor: pointer;
}
.tier input { width: 20px; height: 20px; flex: none; accent-color: var(--ink); align-self: center; }
.tier__band { flex: 1; }
.tier:first-of-type { border-top: 0; }
.tier:has(input:checked) { background: var(--paper); }
.tier:has(input:focus-visible) { outline: 2px solid var(--signal); outline-offset: -2px; }
.tier__band { font-weight: 600; }
.tier__price b { font-family: var(--display); font-weight: 600; font-size: 1.375rem; }
.tier__per { color: var(--steel); font-size: 0.875rem; margin-left: 2px; }

/* ---------- pricing picker (§6.9 / §7.2) ---------- */
.picker { border: 1px solid var(--rule); background-color: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
.picker__row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 56px;
  padding: 12px 16px;
  border-top: 1px solid var(--rule);
}
.picker__row:first-child { border-top: 0; }
.picker__row input[type="checkbox"] { width: 22px; height: 22px; accent-color: var(--signal); flex: none; }
.picker__label { flex: 1; }
.picker__label b { font-weight: 600; }
.picker__price { flex: none; font-weight: 600; }
/* The trial disclosure under the two add-on rows. Takes .picker__row's own
   16px sides so it lines up with the labels above it, and carries the row rule
   on top so it reads as part of the add-on group rather than as a loose caption
   the plate happens to contain. */
.picker__note {
  padding: 12px 16px;
  border-top: 1px solid var(--rule);
  color: var(--steel);
}

/* Running total — the one place the whole configuration adds up. */
.total {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-16);
  justify-content: space-between;
  padding: var(--s-16);
  border-top: 1px solid var(--rule);
  background: var(--paper);
}
.total__figure { min-width: 0; }
.total__amount { margin-top: 6px; }
.total__amount b { font-family: var(--display); font-weight: 600; font-size: 2rem; letter-spacing: -.01em; }
.total__note { margin-top: 4px; color: var(--steel); }

.picker__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  justify-content: space-between;
  padding: 16px;
  border-top: 1px solid var(--rule);
  background: var(--paper);
}
.picker__basis { margin-top: 12px; color: var(--steel); }

/* ---------- accordion (§6.10 / §7.5) ---------- */
/* The builder's own width cap, moved off the section wrap (see pricing.html).
   It is a control panel, not prose: stretched to the full 1160 the tier rows
   put a worker count at one edge and a price at the other with half a metre of
   nothing between them.

   CENTRED, not spine-aligned (owner call, 2026-08-28). It was pinned left at
   164 in a 1160 wrap, leaving a 392px dead gutter down the right of the page's
   main control panel — and the sign-up section directly below it is centred, so
   the page changed alignment halfway down. /faq and /product keep the left
   spine because their content runs the full wrap width; this panel is capped at
   720, and a capped column pinned left reads as a mistake rather than a spine.
   .page-pricing centres the headings above it to match. */
.plan-panel { max-width: 720px; margin-inline: auto; }

/* The two headed sections above the sign-up form follow the panel to centre,
   so the page holds one axis top to bottom. Scoped to .page-pricing: the same
   .section--tight / .h2--major classes carry the left spine on Home, /product
   and /faq, and those pages are staying as they are.

   .measure caps these headings at a reading width; left as-is it centres the
   BLOCK but leaves the text ragged inside it, so the cap gets its own auto
   margins here. The final dark CTA already centres itself and is untouched. */
.page-pricing .section--tight > .wrap,
.page-pricing #pricing > .wrap { text-align: center; }
.page-pricing .section--tight > .wrap > .measure,
.page-pricing #pricing > .wrap > .measure { margin-inline: auto; }

/* ⛔ Centre the HEADINGS and the placement of the plates, never the content
   inside them. text-align inherits, and left unchecked it ran straight down
   into the controls: a tier row centred its band name between the radio and
   the price instead of beside the radio it labels, every form label floated
   over a left-aligned input, and the after-trial paragraph became centred body
   copy. A label belongs to its control, so it starts where the control starts.
   Found in a screenshot, not in the cascade. */
.page-pricing .plan-panel,
.page-pricing #start .demo-plate,
.page-pricing #start .caption,
.page-pricing .picker__basis { text-align: left; }
/* The plate's own headings stay centred with the two above the panel. The
   total's CTA column is NOT listed here: .total is justify-content:space-between
   and .total__act .caption already centres the no-card line, so a rule here
   would be dead weight over layout that is already right. */
.page-pricing .plan-panel > h2,
.page-pricing .plan-panel .h2--major,
.page-pricing .plan-panel .eyebrow { text-align: center; }

/* /sign-up runs the same centred axis as /pricing, which is where its visitors
   have just come from — the two pages read as one flow, so they hold one shape.
   Only the heading block centres; the form inside .demo-plate keeps its labels
   left against their inputs, same rule as the pricing panel. */
.page-sign-up #start > .wrap > #signup-heading,
.page-sign-up #start > .wrap > p[data-signup-aside] { text-align: center; }

/* ⛔ p[data-signup-aside] above, not [data-signup-aside]: the after-trial panel
   carries the same attribute (it has to hide with the form too) and centring
   caught it, turning three lines of prose into ragged centred body copy under a
   left-aligned form. The attribute marks WHAT HIDES, not what centres — the
   one-line demo aside is the only thing here that is short enough to centre. */
.page-sign-up #start > .wrap > .panel { text-align: left; }

/* The success and cancelled panels replace the form, and the dark CTA band below
   hides with it — so the paid state is one short card and the footer rode up the
   screen with a slab of empty ground under it. Holding the viewport height keeps
   the footer where a footer belongs on the one screen a paying customer sees.
   Found in a screenshot of ?checkout=success, not in any test.

   The content CENTRES in that reclaimed height rather than sitting at the top of
   it — min-height alone just moved the empty ground from above the footer to
   below the card, which is the same defect one element down. */
.page-sign-up #start {
  min-height: calc(100vh - var(--sticky-h));
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* The plate the questions rest on. The reading measure the page used to carry
   lives here now, so the answers stay a comfortable line length while the
   heading above keeps the site's left spine. margin-inline-start:0 because the
   plate is aligned to that spine, not centred in the column. */
.faq-list {
  max-width: 760px;
  margin-inline: 0 auto;
  padding-inline: var(--s-24);
  background-color: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

/* Dividers BETWEEN rows only — the plate's own border closes the ends. The list
   previously drew a top rule on every row plus a bottom rule on the last, which
   is what a list does when it has no plate to sit in. */
.faq + .faq { border-top: 1px solid var(--hairline); }
.faq summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding-block: 15px;
  font-weight: 600;
  cursor: pointer;
}
.faq summary::-webkit-details-marker { display: none; }

/* Lit indicator dot — DESIGN.md's signature, the same device the risk badges
   and the PPE toggles use: dim at rest, lit with a glow of its own colour when
   the row is open. This page signalled open/closed with the chevron alone,
   which is the one state signal on the marketing site that was not carrying it.
   Safety orange because open IS the active/selected state, which is the one job
   that colour has. */
.faq summary::before {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--steel-mute);
  transition: background 120ms linear, box-shadow 120ms linear;
}
.faq[open] summary::before {
  background: var(--signal);
  box-shadow: 0 0 6px var(--signal);
}

/* margin-left:auto rather than the container's old space-between: with the dot
   in front there are three items on the line, and space-between would strand
   the question text in the middle of the row. */
.faq summary::after {
  content: "";
  flex: none;
  margin-left: auto;
  width: 12px; height: 12px;
  border-right: 2px solid var(--steel);
  border-bottom: 2px solid var(--steel);
  transform: rotate(45deg);
  transition: transform 120ms linear;
}
.faq[open] summary::after { transform: rotate(-135deg); }
/* Indented to the dot's column so the answer reads as belonging to its question
   rather than starting a new block: 7px dot + the summary's 12px gap. */
.faq__a {
  padding-bottom: 18px;
  padding-left: 19px;
  color: var(--steel);
  max-width: var(--measure);
}

/* ---------- final CTA (§6.11) ---------- */
.final-cta { text-align: left; }

/* ---------- how it works: the four steps (§6.5) ----------
   Each step owns its screen in the markup, which is what lets one structure
   serve two very different layouts: a swipe rail on a phone, and a list beside
   a single cross-fading plate on a desktop. Flexbox throughout — the layout
   suite forbids a multi-column grid at 390px. */
.steps { margin-top: clamp(28px, 4vw, 48px); }

/* MOBILE: the rail. Before this the four screens sat together below the list,
   so at 390px the buttons were ~400px above the image: you tapped a step, saw
   nothing happen, and had to scroll to find out what changed. As cards the
   control and the result are the same object, and the peeking next card is the
   affordance that says swipe — the brief's mobile composition rule.

   The padding/margin-inline pair is the bleed copied from .rail; it is what
   keeps a full-width rail from giving the page horizontal scroll. */
.steps__list {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  padding-inline: 16px;
  margin-inline: -16px;
  /* Pairs with the padding above, and is not optional: a snapport defaults to
     the scroller's padding BOX, so mandatory snap immediately scrolls the 16px
     away and parks card 01 hard against x=0 while the heading above it sits
     inset 16 — with a 16px gutter still showing at the far end, which is what
     made it read as a mistake rather than a bleed. .rail carries the same line
     for the same reason. */
  scroll-padding-left: 16px;
  scrollbar-width: none;
  -ms-overflow-style: none;
  list-style: none;
  margin-block: 0;
}
.steps__list::-webkit-scrollbar { display: none; }
/* The card is HEIGHT-constrained, not 78vw: the screen keeps its 390/844 ratio,
   so the card's width is whatever makes the screen plus its caption clear both
   the sticky nav and the sticky demo bar. 268px is that width, measured at
   390x844. It still leaves ~90px of the next card showing, which is the peek the
   brief asks 78vw to produce. */
.steps__list li {
  flex: 0 0 min(78vw, 268px);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}
.steps__list .steps__plate { order: -1; }   /* screen above its own caption */

@media (min-width: 768px) {
  /* DESKTOP: back to a column beside one plate. The four plates stack in the
     reserve on the right and cross-fade; see the .steps__plate rules below. */
  .steps__list {
    position: relative;
    display: flex;
    flex-direction: column;
    overflow: visible;
    scroll-snap-type: none;
    padding-inline: 0;
    margin-inline: 0;
    padding-right: calc(var(--steps-plate-w) + clamp(28px, 5vw, 64px));
    /* The plates are absolutely positioned and contribute no height, but the
       four rows still have to run the plate's full height or the hairlines lose
       their pitch and the rows bunch at the top of the band. 844/390 is the
       captures' aspect ratio. */
    min-height: calc(var(--steps-plate-w) * 844 / 390);
  }
  /* Equal quarters, not space-between: space-between spreads the rules apart but
     leaves each row's copy pinned to the top of its own gap. Giving every li an
     equal share and centring the step inside it puts the four rules on a fixed
     pitch with the copy sitting on each one's centreline. */
  .steps__list li { flex: 1; display: flex; }
  .steps__list .step { flex: 1; align-items: center; }
}
.step__body { max-width: 46ch; }
/* In card form the step is a caption under its own screen, so it carries no
   hairlines: the rules exist to bind a column of rows together, and there is no
   column on mobile. They come back with the desktop layout below. */
/* THE GAP HAS TO CLEAR THE LAMP, NOT JUST THE NUMBER. .step__n::after hangs a
   5px dot at right: -11px, so it eats 11px of whatever this gap is: at the 16px
   this shipped with, the dot finished 5px from the title and the two read as one
   crowded unit. 24px leaves 13px of air, 32px on desktop leaves 21px — measured
   from the dot's edge, which is the thing you actually see, not the number's. */
.step {
  display: flex;
  gap: var(--s-24);
  align-items: baseline;
  color: inherit;
  text-decoration: none;
  transition: border-color 120ms linear;
}
@media (min-width: 768px) {
  .step {
    gap: var(--s-32);
    padding-block: clamp(14px, 1.8vw, 20px);
    border-top: 1px solid var(--rule);
  }
  .steps__list li:last-child .step { border-bottom: 1px solid var(--rule); }
}
.step__n {
  flex: none;
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  color: var(--steel);
  transition: color 120ms linear;
}
.step__body { display: flex; flex-direction: column; gap: 3px; min-width: 0; max-width: 46ch; }
/* The one place the Archivo width axis goes narrow. The subset is instanced
   `font-stretch: 85% 110%` and until now only 110% was ever drawn, so the
   condensed end shipped on every page load unused. These four one-word labels
   sit in a control row with a lit indicator beside each — an equipment
   nameplate, literally, not a metaphor for one — and they sit directly under
   .h2--major at 110%, which is the only way a width decision reads as a
   decision rather than a rendering accident. */
.step__t {
  font-family: var(--display);
  font-weight: 600;
  font-stretch: 85%;
  font-size: 1.375rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 1.1;
}
.step__s { color: var(--steel); font-size: 0.9375rem; line-height: 1.4; }

/* The active step is a lit control: the number goes orange and the dot lights.
   DESIGN.md defines this device as "unlit (dim/low-opacity) at rest and lit
   (full opacity, brighter glow) on selection", so the dot is present on every
   step and changes state — it used to exist only under [aria-current], which
   made it pop into being rather than power on. The glow is a real 4px blur per
   the Lit-Not-Flat rule; the old 0 0 0 2px was a flat ring.

   A transition, not a keyframe, on purpose: the reduced-motion block at the
   foot of this file kills animation-duration outright, so a keyframed glow
   would not exist at all for those users, whereas a transition degrades to the
   correct end state instantly. */
.step__n { position: relative; }
.step__n::after {
  content: "";
  position: absolute;
  top: 50%;
  right: -11px;
  width: 5px;
  height: 5px;
  margin-top: -2.5px;
  border-radius: 50%;
  background: var(--steel-mute);
  transition: background-color 160ms linear, box-shadow 160ms linear;
}
.step[aria-current="true"] .step__n { color: var(--signal-ink); }
.step[aria-current="true"] .step__n::after {
  background: var(--signal);
  box-shadow: 0 0 4px var(--signal), 0 0 0 2px rgba(255,107,0,0.20);
}
.step:hover .step__n { color: var(--signal-ink); }
.step:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; }

/* ---------- the steps on the graphite console ----------
   None of this is polish; every line is a contrast failure on the dark ground.
   --rule (#DAD7CE) is near-white on graphite and draws four glaring hairlines.
   --steel (#5C5A54) on #23262B is about 2.2:1 and fails AA. --signal-ink
   (#993F00) is about 1.6:1, which would leave the LIT step less legible than
   the unlit ones — so the active state takes --orange-lit, which exists in
   :root for exactly this and is already used this way by .section--dark
   .eyebrow. */
.section--dark .step { border-top-color: rgba(255,255,255,0.13); }
.section--dark .steps__list li:last-child .step { border-bottom-color: rgba(255,255,255,0.13); }
.section--dark .step__n,
.section--dark .step__s { color: var(--steel-mute); }
.section--dark .step[aria-current="true"] .step__n,
.section--dark .step:hover .step__n { color: var(--orange-lit); }
/* app-01-open.webp is itself a full-graphite screen and it is the .is-on state
   every visitor lands on. .steps__plate deliberately carries no
   .phone-plate__header (see the markup comment), so on this band there is
   nothing else to hold the plate's edge.

   This used to be `border: 1px solid rgba(255,255,255,0.14)` and it read as a
   design error, because it was the ONLY thing separating the plate from the
   band: .phone-plate fills #23262B and --graphite IS #23262B, so the plate had
   no body of its own. A hairline with nothing behind it is not an edge, it is a
   drawn outline — and at 1x DPR that single antialiased pixel crawls around the
   20px radius, which is what made it look thin and pixelated.

   The fix is to give the plate a body instead of a stroke. --graphite-2 is
   DESIGN.md's "gradient partner to Graphite Plate on dark screens", so the plate
   sits one step above the console it rests on, and the edge comes from the
   system's own plate vocabulary: a lit top bevel and a contact shadow, per the
   Contact-Shadow-Not-Float rule.

   That was right and it still did not show, because the plate had no PADDING:
   the capture measured 300x649 in a 300x649 plate, so the body, the bevel and
   the edge were all painted underneath it. On step 01 the screen's top edge is
   #22262B against a #23262B band — one unit, in one channel — and the phone
   disappeared into the console. The markup calls this plate "just the bezel";
   this is the rule that gives the bezel width, and with 10px of body behind it
   the 1px edge is no longer a drawn outline but the plate vocabulary doing its
   ordinary job.

   The old third shadow (0 24px 60px -20px) is gone. A soft ambient blur is what
   Contact-Shadow-Not-Float exists to ban, and a BLACK one on graphite rendered
   nothing anyway. A glow was asked for and ruled out: --shadow-glow is orange,
   and DESIGN.md keeps orange for action and the glow for the one primary lit
   control on a screen. */
.section--dark .steps__plate {
  padding: 10px;
  background: var(--graphite-2);
  border: 1px solid rgba(255,255,255,0.14);
  box-shadow:
    0 1px 0 rgba(255,255,255,0.10) inset,
    0 2px 0 rgba(0,0,0,0.35);
}

/* max-height:none overrides .phone-plate's 68vh cap. That cap exists for the
   hero, whose screen scrolls; here the plate holds one fixed-ratio capture, and
   clipping it left a white sliver of the next image showing under the submit
   bar. The screens box sets the height instead.

   On mobile each plate is simply the top half of its own card: full card width,
   no stacking, always visible. */
/* Fills its card, so the screen and the caption beneath it share a left edge.
   The card's own width is what does the sizing — see .steps__list li. */
.steps__plate {
  width: 100%;
  max-width: none;
  max-height: none;
}
/* Deliberately paints nothing. This was `background: var(--surface)` — a WHITE
   rectangle filling a plate that clips at a 20px radius, so every corner arc
   antialiased white against the plate and left a light, crawly fringe on all
   four corners (worst on the top two at desktop, all four in the mobile rail).
   The white was never visible otherwise: .steps__screen is object-fit:contain
   in a box holding the captures' exact 390/844 ratio, so there is no letterbox
   for it to fill. Leaving it transparent lets the plate's own fill show, which
   is also the better placeholder while the image loads and the better letterbox
   if a future capture ever has a different ratio.

   The radius is the plate's 20px minus its 10px of bezel, so the two arcs stay
   concentric — square corners inside a rounded shell read as a mistake. The ring
   is what sets the screen INTO the body: on the three light captures it is the
   line between screen and bezel, and on the dark sign-in capture the bezel does
   that job instead. */
.steps__screens {
  position: relative;
  aspect-ratio: 390 / 844;
  background: transparent;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.35);
}
.steps__screen {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* contain, not cover: these are 390x844 phone captures and the box holds that
     exact ratio, so cover only risked cropping the submit bar flush against the
     plate edge — which read as a clipping fault rather than a screenshot. */
  object-fit: contain;
  object-position: top center;
}

@media (min-width: 768px) {
  /* The four plates stack in the reserve the list keeps clear on the right, and
     the PLATE is what cross-fades — not the image inside it. Each plate draws
     opaque graphite chrome, so fading the images would leave plate 04's
     background sitting on top of the other three and nothing would ever show. */
  .steps__plate {
    position: absolute;
    top: 0;
    right: 0;
    width: var(--steps-plate-w);
    opacity: 0;
    transition: opacity 160ms linear;
  }
  .step[aria-current="true"] + .steps__plate { opacity: 1; }
  /* No JS: the steps are still anchors, so :target switches the plate. Both
     rules are scoped to the same ancestor on purpose — step 01 keeps
     aria-current in the markup, so without this the targeted plate and plate 01
     would both be opaque and the screens would ghost through each other. With
     JS the click is preventDefault'd and :target never fires. */
  .steps__list:has(.steps__screen:target) .steps__plate { opacity: 0; }
  .steps__list:has(.steps__screen:target) .steps__plate:has(.steps__screen:target) { opacity: 1; }
}

/* ---------- module mockups (§6.7) ----------
   Drawn in markup rather than screenshotted: crisp at any DPI, a fraction of the
   weight of the WebP captures they replace, and they cannot drift out of date
   the way a stale screenshot does. Each one is decorative — the <div> carries
   role="img" + aria-label, and everything inside is aria-hidden, so a screen
   reader gets one honest description instead of fake UI text.

   Flexbox only, deliberately: the layout tests forbid any element resolving to a
   multi-column grid at 390px, and these sit inside a 78vw rail card. */
.mock {
  height: 200px;
  padding: var(--s-12);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;   /* short lists centre rather than stranding dead space */
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  font-size: 11px;
  line-height: 1.35;
  color: var(--ink);
}
.mock-line { height: 1px; background: var(--rule); }

/* 1 — form library: three sheets on the desk, not one flat scan */
.mock--library { display: flex; align-items: center; justify-content: center; }
.mock-stack { position: relative; width: 116px; height: 152px; }
.mock-sheet {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 11px 10px;
  background: var(--surface);
  border: 1px solid var(--rule);
}
.mock-sheet:nth-child(1) { transform: translate(-15px, 5px) rotate(-5deg); }
.mock-sheet:nth-child(2) { transform: translate(-7px, 2px) rotate(-2deg); }
.mock-sheet__t { height: 5px; width: 62%; background: var(--ink); }
.mock-sheet__r { height: 3px; background: var(--rule); }
.mock-sheet__sig { margin-top: auto; height: 3px; width: 48%; background: var(--steel); }

/* 2 — custom form builder: the editor mid-edit (.fe-sec / .fe-row) */
.mock-sec { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius-sm); overflow: hidden; }
.mock-sec__head {
  display: flex;
  align-items: center;
  gap: var(--s-8);
  padding: 7px 9px;
  background: var(--plate-face);
  border-bottom: 1px solid var(--rule);
}
.mock-badge {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .06em;
  padding: 2px 5px;
  background: rgba(20, 24, 43, .08);
}
.mock-sec__title { font-weight: 600; }
.mock-sec__count { margin-left: auto; font-family: var(--mono); font-size: 9px; color: var(--steel); }
.mock-row {
  display: flex;
  align-items: center;
  gap: var(--s-8);
  padding: 7px 9px;
  border-bottom: 1px solid var(--rule);
}
.mock-row:last-child { border-bottom: 0; }
.mock-row__icon { width: 18px; height: 18px; flex: none; background: rgba(20, 24, 43, .08); }
.mock-row__pill {
  margin-left: auto;
  font-size: 9px;
  color: var(--steel);
  padding: 2px 7px;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 999px;
}

/* 3 — ticket tracker · 5 — submission history: hairline record lists */
.mock-list { background: var(--surface); border: 1px solid var(--rule); }
.mock-head {
  display: flex;
  gap: var(--s-8);
  padding: 6px 9px;
  border-bottom: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .06em;
  color: var(--steel);
}
.mock-head__r { margin-left: auto; }
.mock-item {
  display: flex;
  align-items: center;
  gap: var(--s-8);
  padding: 7px 9px;
  border-bottom: 1px solid var(--rule);
}
.mock-item:last-child { border-bottom: 0; }
/* Name over detail — as inline spans these ran together ("D. FerrisFall protection"). */
.mock-item__main { display: flex; flex-direction: column; min-width: 0; }
.mock-item__t { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mock-item__s { color: var(--steel); font-size: 10px; }
.mock-item__meta { margin-left: auto; flex: none; font-family: var(--mono); font-size: 9px; color: var(--steel); }
/* The expiring ticket. Weight and a filled chip carry the emphasis — orange is
   reserved for controls, and a status is not one. */
.mock-item__meta--flag {
  color: var(--paper);
  background: var(--ink);
  padding: 2px 6px;
}

/* 4 — time clock: chip, live counter, and the one real control in the set */
.mock--clock { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--s-8); }
.mock-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .06em;
  color: var(--steel);
}
.mock-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--signal); }
.mock-clock__time { font-family: var(--mono); font-size: 30px; letter-spacing: -.02em; }
.mock-clock__sub { color: var(--steel); font-size: 10px; }
.mock-btn {
  margin-top: var(--s-4);
  padding: 8px 18px;
  border-radius: 4px;
  background: var(--signal-dark);
  color: #fff;
  font-weight: 600;
  font-size: 11px;
}

/* ---------- footer (§6.12) ---------- */
.footer { background: var(--ink); color: var(--paper); }
/* Edge to edge, matching .nav__inner — same reasoning and the same gutter steps.
   The ink band already spanned the viewport while its contents were capped at
   1160, so on a 1920 screen the mark floated 380px inboard of a band that did
   not. Owner call, 2026-08-17.

   Scoped as a child selector (0,2,0) rather than leaning on source order the way
   .nav__inner has to: the footer's inner div carries no class of its own, and
   this way the rule cannot be defeated by someone moving .wrap down the file. */
.footer > .wrap { max-width: none; }
@media (min-width: 1280px) {
  .footer > .wrap { padding-inline: var(--s-32); }
}

/* Stacked on a phone, where the links already need three rows and the mark has
   nowhere to sit beside them. */
.footer__inner { display: grid; gap: var(--s-16); }
.footer__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-16) var(--s-24);
  font-weight: 500;
}

/* One row from 768, mark and links at opposite edges — the nav bar's treatment,
   for the same reason. Lifting the 1160 cap alone moved the footer's contents
   left instead of spreading them, because they are a left-aligned column with
   nothing pushing right: the links ended at x=837 whatever the screen, leaving
   56% of a 1920 viewport empty on one side and reading as an unfinished row.
   space-between is what makes the band actually span. align-items centres the
   32px mark against the link row so it sits ON the line of the text, not above
   it. */
@media (min-width: 768px) {
  .footer__inner {
    grid-auto-flow: column;
    align-items: center;
    justify-content: space-between;
  }
  .footer__nav { justify-content: flex-end; }
}
.footer a { color: var(--paper); }
.footer a:hover { border-bottom: 1px solid var(--signal); }

/* The sticky mobile CTA bar (§5.3) was removed on 2026-08-28. Its
   scroll-padding-bottom went with it: the 72px existed so an anchored element
   would not land under the bar, and there is nothing down there now. */

/* ---------- 404 (NEAT-252 · D6) ----------
   One file, two faces. public/404.html is the body Vercel returns for every
   not-found response on EVERY host: the filesystem answers before any rewrite,
   and that fallback takes no host condition. NEAT-237 §6 keeps company
   subdomains on the app's own behaviour, so host-face.js sets data-face="app" on
   <html> for any host that is not www/stg/apex, and these rules swap the faces.

   Visibility has exactly one owner, and it is `display` in this block. Neither
   face carries a `hidden` attribute, on purpose: this file sets `display` on
   both, and an author display beats the UA sheet's [hidden] { display: none }.
   That is the trap the utilities note below is about, and two mechanisms
   answering one question is how it keeps coming back.

   The nav and the footer go with the marketing face. Every href in them is
   root-relative (/product, /pricing, /#demo-form), and on a company subdomain
   each one resolves to the app shell, so leaving them up would hand a worker a
   row of links that go nowhere near where they read. */
.page-404 [data-face-app] { display: none; }

[data-face="app"] .page-404 [data-face-marketing] { display: none; }
[data-face="app"] .page-404 [data-face-app] { display: block; }
[data-face="app"] .page-404 .nav,
[data-face="app"] .page-404 .footer { display: none; }

/* html carries --ink so the canvas past the end of the document reads as the
   footer band continuing (see the note on that rule above). The app face hides
   the footer, so there is no band to continue and that same rule painted an
   empty graphite slab under three lines of text — the whole lower two thirds of
   a phone. Found in a screenshot; every assertion about the swap was green.

   So the app face takes the steel ground on the canvas too, and fills the
   viewport rather than clinging to the top of it. Centred, because a dead end
   that sits at the very top of an otherwise empty page reads as a page that
   failed to load rather than one that is telling you something. */
html[data-face="app"] { background: var(--paper); }
[data-face="app"] .page-404 #not-found {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  align-content: center;
}

/* The signpost: one row per page, with a line saying what is on it. Rules rather
   than cards, and five rows rather than three, because a three-up card grid is
   on the banned-compositions list in the surface brief. .dl next door looked
   like a fit and is not — its mono uppercase <dt> would have drawn the links as
   small grey labels, and these are the way out of the page.

   The whole row is the anchor, so the tap target is the row and not the word.
   PRODUCT.md sets the floor at 44px on this surface. */
.signpost { display: grid; list-style: none; }
.signpost > li { border-top: 1px solid var(--rule); }
.signpost > li:last-child { border-bottom: 1px solid var(--rule); }

.signpost__row {
  display: block;
  padding-block: 12px;
  min-height: 44px;
  color: var(--ink);
}
/* inline-block so the hover underline hugs the word instead of running the full
   width of the row. Same device as .link-out: a transparent border holds the
   space at rest, so nothing shifts when it lights. */
.signpost__name {
  display: inline-block;
  font-weight: 600;
  border-bottom: 1px solid transparent;
}
.signpost__row:hover .signpost__name { border-bottom-color: var(--signal); }
.signpost__what {
  display: block;
  margin-top: 2px;
  font-size: 0.875rem;
  line-height: 1.45;
  color: var(--steel);
}

/* ---------- utilities ----------
   `hidden` has to actually hide.

   The UA stylesheet's [hidden] { display: none } is the weakest rule in the
   cascade, so ANY author rule that sets `display` on the same element beats it.
   Every .btn is display:inline-flex, so `el.hidden = true` on a button left it on
   screen and clickable while the DOM reported hidden === true — a state check in
   the console said the trial CTA was gone on the quote-only tier, and the
   screenshot showed it sitting there.

   This file had already worked around the same trap twice per component
   (.form-lightbox[hidden], .miniform-gate[hidden]). This is the general rule, and
   it lives here rather than in the reset because [hidden] and a class carry equal
   specificity — at the top it would lose to .btn, at the bottom it wins on source
   order. No !important, per the note at the top of this file. */
[hidden] { display: none; }

/* ---------- reduced motion (§4.5) ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}
