/* ===========================================================================
   Marketing v4 — shared token + primitive layer.

   PLAN_MARKETING_V4_BUILD_PHASE1_2026-08-24.md S1: fixes v3's no-external-
   stylesheet debt (v3's shared CSS lives inline in landing/v3/_base.html's
   <style> block; every byte re-downloads uncached on every page). This file
   is the ONE shared layer, loaded by web/templates/landing/v4/_base.html at
   /static/css/marketing-v4.css?v=<N> — bump the ?v= query param on every
   edit (cache-bust convention, .claude/rules/frontend.md).

   Scope is intentionally narrow — selectors/values are lifted byte-faithful
   from the locked contract, docs/mockups/marketing_v4_homepage_LOCKED_2026-
   08-24.html: the :root token block, resets, .wrap, .mono, .eyebrow,
   .btn/.btn-primary/.btn-secondary, .dot/.tag/.chip, .card, the nav rules,
   the footer rules. Everything else (hero, proof band, pricing table, ...)
   is PAGE-SPECIFIC and stays OUT of this file — page templates bring it via
   {% block page_styles %}, same mechanism v3 uses today.

   No responsive rules are ported here because the locked mockup does not
   define any for nav/footer (verified: its only two @media blocks target
   page-specific selectors, .rp-prog and .cmp — neither is in this file's
   scope). A mobile nav pattern is not yet part of the locked contract.
   =========================================================================== */

:root {
  /* --- app tokens, verbatim from web/static/css/main.css (per the locked
         mockup's own comment: "Tokens are lifted verbatim ... so a future
         product-parity pass is a drop-in, not a rewrite.") --- */
  --primary-900: #0a2540; --primary-800: #0c3354; --primary-700: #0052a3;
  --primary-600: #1560BC; --primary-500: #1976D2; --primary-400: #4299e1;
  --primary-300: #7eb8f5; --primary-200: #bfdbfe; --primary-100: #dbeafe; --primary-50: #EBF5FF;
  --success: #10b981; --success-dark: #065f46;
  --warning: #f59e0b; --caution: #B45309;              /* the true amber, not a red-brown */
  --error: #ef4444;   --error-dark: #991b1b;
  --observation: #0EA5E9; --observation-dark: #0369a1;
  --gray-50: #f9fafb; --gray-100: #f3f4f6; --gray-200: #e5e7eb; --gray-300: #d1d5db;
  /* gray-500 darkened from #6b7280 on 2026-08-26: as body text on --canvas it
     measured 4.40:1, just under the 4.5:1 AA floor, across 68 text nodes on 11
     pages. #646c7a measures 4.82 on canvas and 5.29 on white. */
  --gray-400: #9ca3af; --gray-500: #646c7a; --gray-600: #4b5563; --gray-700: #374151;
  --gray-800: #1f2937; --gray-900: #111827;
  --canvas: #F4F4F6;
  --shadow-sm: 0 1px 2px rgba(15,23,42,.06);
  --shadow-md: 0 1px 3px rgba(15,23,42,.08), 0 4px 12px rgba(15,23,42,.04);
  --shadow-lg: 0 4px 8px rgba(15,23,42,.10), 0 12px 24px rgba(15,23,42,.06);
  --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; --radius-xl: 12px;
  --focus: 0 0 0 3px rgba(21,96,188,.35);

  /* --- MARKETING scale. The app scale stops at 24px because it is a dense
         data UI with a 12px metadata floor. A marketing page lives above
         that. --- */
  --m-display: 60px; --m-h2: 40px; --m-h3: 24px;
  --m-lede: 20px; --m-body: 17px; --m-dense: 15px; --m-meta: 13px;

  --sans: "IBM Plex Sans", -apple-system, "Segoe UI", sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, Consolas, monospace;

  /* --- VERTICAL RHYTHM. Added 2026-08-26 (PLAN_MARKETING_V4_POLISH T2-2).
         Until now this layer carried tokens for colour, type, radius and
         shadow but NONE for space, so all 18 page templates improvised their
         own section padding inline: 121 declarations across 17 distinct
         values (32,34,36,40,44,48,52,56,60,64,68,72,76,80,84,88,96) — a 4px
         random walk, not a scale. That is the structural cause of the site
         reading as "assembled". Four steps, 8px grid:
           xl  section that anchors a movement (the dark panels, pricing)
           lg  the standard section
           md  a compact section or a strip
           sm  a hairline-separated band                                   --- */
  --sp-xl: 88px; --sp-lg: 64px; --sp-md: 44px; --sp-sm: 28px; --sp-xs: 20px;
  /* Tightened 2026-08-26. The first values (112/88/56/36/24) measured a MEDIAN
     GAP OF 200px between sections — a section's bottom padding stacks with the
     next one's top padding, so two --sp-lg neighbours already spent 176px, and
     lg-next-to-xl spent 200. On a 900px viewport that is a fifth of the screen
     empty at every seam. These land the median near 150.
     xs is for chrome rails, not sections — a thin "also serving" strip or a
     one-line band. Added when the industry pages' 22px rails would otherwise
     have been rounded up to 36px purely to fit the scale, which is the tail
     wagging the dog. */
}

/* ============== reset ============== */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  font-family: var(--sans); color: var(--gray-900); background: var(--canvas);
  -webkit-font-smoothing: antialiased; line-height: 1.55;
}
a { color: var(--primary-600); text-decoration: none; }
a:hover { color: var(--primary-700); }
img { display: block; max-width: 100%; }
/* A bare height attr maps to PRESENTATIONAL CSS height, so an img carrying
   width/height attrs (which it should, to prevent layout shift) stretches or
   squashes the moment a rule sets width:100% without also setting height.
   That shipped once already — PR #2811, figures rendering 3.7x tall. Default
   it away here; any page rule with a class still out-specifies this. */
img[width][height] { height: auto; }
:focus-visible { outline: none; box-shadow: var(--focus); border-radius: var(--radius-sm); }

/* ============== layout primitive ============== */
.wrap { max-width: 1200px; margin: 0 auto; padding: 0 44px; }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* Section rhythm. A page template picks a step; it never types a pixel value.
   The modifiers are separate classes rather than a custom-property override so
   the choice is greppable and lintable — "which sections anchor a movement?"
   has to be answerable without rendering the page. */
.v4-section { padding: var(--sp-lg) 0; }
.v4-section--anchor { padding: var(--sp-xl) 0; }
.v4-section--tight  { padding: var(--sp-md) 0; }
.v4-section--strip  { padding: var(--sp-sm) 0; }
.v4-section--rail   { padding: var(--sp-xs) 0; }
/* Asymmetric openers/closers: a section that meets a hairline above but a full
   band below, and the reverse. Both steps still come from the scale. */
.v4-section--open   { padding: var(--sp-md) 0 var(--sp-lg); }
.v4-section--close  { padding: var(--sp-lg) 0 var(--sp-md); }

/* ============== eyebrow ============== */
/* Doubled class on purpose. Five page templates carry a broad
   `.section p { font-size: ... }` rule, which is (0,1,1) and beats a bare
   `.eyebrow` at (0,1,0) — so on five of the v4 pages the eyebrow silently
   inherited BODY size. It measured 17px next to the same eyebrow at 12px two
   sections away, which is what "these headers look a little silly" was seeing.
   (0,2,0) is above any single-class descendant rule, so a layout rule can no
   longer capture it by accident, while a DELIBERATE override — `.proof.dark
   .eyebrow`, `.ind-find .eyebrow` — still wins, being (0,2,0) or higher and
   later in the cascade. */
.eyebrow, .eyebrow.eyebrow {
  font-family: var(--mono); font-size: 12px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--primary-600); margin-bottom: 16px; font-weight: 500;
}

/* ============== severity/status vocabulary (app's own grammar) ============== */
.dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; flex: none; background: currentColor; }
.tag {
  display: inline-flex; align-items: center; gap: 7px; font-size: var(--m-meta); font-weight: 600;
  letter-spacing: .005em;
}
.chip {
  display: inline-flex; align-items: center; gap: 7px; font-size: var(--m-meta); font-weight: 600;
  padding: 4px 11px; border: 1px solid currentColor; border-radius: var(--radius-md); background: transparent;
}

/* F1-1 (five-lens review, 2026-08-25): the five severity color modifiers the
   locked mockup defines right beside .dot/.tag/.chip (~lines 58-59). These
   were dropped in the S1 port -- every .tag/.chip inherited plain body ink
   (or, inside a dark section like .proof.dark, the section's own light-blue
   ink) instead of its severity color. */
.major_nc { color: var(--error-dark); }
.minor_nc { color: var(--caution); }
.observation { color: var(--observation-dark); }
.ok { color: var(--success-dark); }
.neutral { color: var(--gray-500); }

/* ============== controls (44px target, not the app's 36px row) ============== */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px; font: inherit;
  font-size: var(--m-dense); font-weight: 600; border-radius: var(--radius-md); padding: 12px 22px;
  min-height: 44px; border: 1px solid transparent; cursor: pointer; white-space: nowrap;
  transition: background .15s, border-color .15s, box-shadow .15s, transform .15s;
}
.btn-primary { background: var(--primary-600); color: #fff; box-shadow: var(--shadow-sm); }
.btn-primary:hover { background: var(--primary-700); color: #fff; box-shadow: var(--shadow-md); transform: translateY(-1px); }
.btn-secondary { background: #fff; color: var(--gray-800); border-color: var(--gray-300); box-shadow: var(--shadow-sm); }
.btn-secondary:hover { border-color: var(--gray-400); color: var(--gray-900); box-shadow: var(--shadow-md); }
/* Keyboard focus on buttons (WCAG 2.4.7). The global :focus-visible ring above is
   delivered as a box-shadow, and .btn-primary / .btn-secondary set their own
   box-shadow at equal specificity LATER in this sheet, so on source order the
   resting shadow wins and every CTA on the site had no ring (measured 2026-08-27:
   13 of 53 focusables, all of them buttons). A two-ring shadow reads on both
   grounds: the white ring shows on navy, the blue ring on white. */
/* Element-qualified so a page-level ground override such as `.next .btn-secondary
   { box-shadow: none }` (same specificity, later sheet) cannot cancel the ring;
   the nav's own `.nav .btn-primary:focus-visible` still outranks it, as intended. */
a.btn:focus-visible, button.btn:focus-visible { box-shadow: 0 0 0 2px #fff, 0 0 0 5px var(--primary-600); }

/* ============== surfaces ============== */
.card {
  background: #fff; border-radius: var(--radius-lg); box-shadow: var(--shadow-md);
  border: 1px solid rgba(15,23,42,.05);
  transition: transform .14s ease, box-shadow .14s ease, border-color .14s ease;
}
/* Motion vocabulary (2026-08-25, owner-approved D4/R1/R7): fast, eased,
   subtle hover lift — sitewide "scientific instrument" register. */
.card:hover {
  transform: translateY(-2px); box-shadow: var(--shadow-lg); border-color: rgba(15,23,42,.12);
}

/* ============== nav ============== */
/* Navy chrome (2026-08-26, owner-approved). --primary-900 is already the ink
   of the hero reading panel, the evidence band and the footer, so the page now
   opens and closes in one colour instead of fading in from white. Two known
   consequences, both accepted at the decision:
     - Scrolled over a navy section the bar recedes into it; the white CTA is
       what anchors the chrome there. Revert = restore the .nav background.
     - The hero card is no longer the only dark object above the fold. */
.nav {
  height: 68px; background: rgba(10,37,64,.94); backdrop-filter: saturate(180%) blur(8px);
  border-bottom: 1px solid rgba(255,255,255,.18); position: sticky; top: 0; z-index: 50;
}
.nav .wrap { display: flex; align-items: center; gap: 36px; height: 68px; }
.brand {
  display: flex; align-items: center; gap: 10px; font-size: var(--m-lede); font-weight: 600; color: #fff;
  letter-spacing: -0.02em;
}
/* The three-bar mark, restored 2026-08-30. History, because it will look like a
   regression to anyone who only reads the v4 commits:

   The mark is three staggered bars plus a dot, and it shipped that way in the
   app sidebar (403ea8173, 2026-05-12) and on the v3 marketing nav two days later
   (9c7fe32c4). The v4 rebuild (971fd4310, 2026-08-24) authored `.brand` fresh as
   two CSS bars — 16x2 over 10x2, --primary-600 over --primary-400 — and the third
   bar and the dot were gone. Nothing recorded that: the mark appears in no
   PLAN_MARKETING_V4_*, not in REVIEW_MARKETING_V4_NAV_2026-08-26 (which discusses
   only the brand's SPACING), and not in that review's closeout. Everything after
   only rescaled the two bars (22/14, 3px) and recoloured them for navy. The site
   was the only surface disagreeing with itself: base.html, signin.html and
   signup.html have carried the three-bar SVG the whole time.

   The graduated opacity is deliberate and is NOT the "white at alpha" treatment
   the two-bar note below rejected. That objection was about a two-bar LIGHTNESS
   pair, where the second bar reads as a disabled copy of the first. Three bars at
   .55 / 1 / .75 read as depth, and the app sidebar has rendered exactly these
   values on a navy field since May. Same file, same values, one mark.

   The superseded two-bar note, kept because its colour reasoning still binds any
   future flat treatment: "The mark's two bars are a HUE pair, not a lightness
   pair — on white they were --primary-600 over --primary-400. Carrying that
   across as white over 50% white turned an accent into a transparency fade,
   which reads as a disabled control rather than as part of the logo." */
/* No `color` here: the rects are `currentColor`, so the mark follows the
   wordmark's ink rather than pinning its own. */
.brand svg { flex: none; }
/* 20px, not 8px: at 8px the brand sat 44px from `Product` against 28px
   between links — 1.57x an inter-link gap, which is not enough to stop the
   wordmark reading as the sixth nav item. 56px total gives it its own zone. */
.links { display: flex; gap: 28px; margin-left: 20px; }
/* Ink on navy comes from the ladder the rest of the site already uses —
   #DCEBFA / #A9C4DE / #9FB6CC / #8FB4DA (evidence band, reading panel, footer).
   Every rung is BLUE-BIASED. White-at-alpha is not on that ladder: it composites
   to a neutral blue-grey (74% over this navy = #BFC6CD), which passes AA at
   7.9:1 and still reads washed next to every other navy surface on the page.
   Nav links take the brightest rung; "Sign in" sits one rung down so the bar has
   a hierarchy rather than three things at one weight. */
.links a { font-size: var(--m-dense); color: #DCEBFA; font-weight: 500; transition: color .12s ease; }
.links a:hover, .links a.on { color: #fff; }

/* Underline slide — scoped to DIRECT children of .links (the flat Pricing
   link) only, never bare `.links a`: that selector also matches every
   anchor inside an .nvd-p dropdown panel (a nested descendant of .links),
   which already has its own background-hover treatment and no
   position:relative to anchor an ::after against. */
.links > a { position: relative; }
.links > a::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -6px; height: 2px;
  background: rgba(255,255,255,.45); transform: scaleX(0); transform-origin: left;
  transition: transform .16s ease;
}
.links > a:hover::after, .links > a:focus-visible::after, .links > a.on::after { transform: scaleX(1); }
/* Hover and current-page must not be the same picture. Both draw the underline;
   only `.on` draws it at full white, and only `.on` holds it with no pointer on
   the bar. A hovered sibling then reads as an echo of the current item rather
   than a second one. This is not colour-alone (1.4.1) — persistent-vs-transient
   is the signal, and `.on` is the only one showing when nothing is hovered. */
.links > a.on::after { background: #fff; }
/* 26px: at 18px the `Sign in` text link sat close enough to the filled CTA to
   read as part of it. These are two different doors — existing customer and
   new visitor — and the gap is what says so. */
.navr { margin-left: auto; display: flex; align-items: center; gap: 26px; }

/* "Sign in" — these three declarations lived in an inline style attribute on
   the anchor in v4/_base.html, where an inline style beats every stylesheet
   rule and the nav's own colour could not reach it. */
.navr > a:not(.btn) { font-size: var(--m-dense); color: #A9C4DE; font-weight: 500; transition: color .12s ease; }
.navr > a:not(.btn):hover { color: #fff; }

/* The primary CTA inverts on the navy bar: white pill, navy label. Elsewhere
   on the page .btn-primary stays the solid --primary-600 fill. */
.nav .btn-primary { background: #fff; color: var(--primary-900); box-shadow: none; }
.nav .btn-primary:hover {
  background: #DCEBFA; color: var(--primary-900); box-shadow: none; transform: translateY(-1px);
}

/* Keyboard focus on the navy bar (WCAG 2.4.7). The global :focus-visible ring
   is a --primary-600 glow at 35% that all but disappears on navy, and
   `outline: none` has already removed the UA's own indicator — so each ground
   in the nav needs its own ring, and a blanket `.nav :focus-visible` is wrong
   because the nav contains two different grounds:
     - bar controls sit on navy            -> white ring
     - the CTA is a white pill             -> navy gap first, or a white ring
                                              just merges into the pill
     - the desktop dropdown is a white card -> left on the global blue ring;
                                              re-whitened in the phone
                                              accordion below, where the panel
                                              is transparent over navy again */
.brand:focus-visible,
.nvd-t:focus-visible,
.links > a:focus-visible,
.navr > a:not(.btn):focus-visible { box-shadow: 0 0 0 3px rgba(255,255,255,.5); }
.nav .btn-primary:focus-visible {
  box-shadow: 0 0 0 2px var(--primary-900), 0 0 0 5px rgba(255,255,255,.75);
}

/* ---- phone drawer trigger + panel ----
   Below 768px .links and .navr collapse behind this, so the bar stays 68px
   instead of wrapping to 223px (measured at 390px wide before this landed). */
.nav-burger {
  display: none; margin-left: auto; flex-direction: column; justify-content: center;
  gap: 4px; width: 44px; height: 44px; padding: 0 10px; background: none;
  border: 0; cursor: pointer;
}
.nav-burger span { display: block; height: 2px; background: #fff; border-radius: 1px; }
.nav-burger:focus-visible { box-shadow: 0 0 0 3px rgba(255,255,255,.5); }

.nav-drawer {
  display: none; position: fixed; inset: 68px 0 0; z-index: 60;
  background: var(--primary-900); padding: 4px 18px 32px; overflow-y: auto;
  overscroll-behavior: contain;
}
body.nav-drawer-open .nav-drawer { display: block; }
/* Scoped, never a generic body.modal-open: that class is ref-counted elsewhere
   and this would decrement someone else's lock. */
body.nav-drawer-open { overflow: hidden; }
.nav-drawer .nvd-h {
  font-family: var(--mono); font-size: 11px; font-weight: 600; letter-spacing: .10em;
  text-transform: uppercase; color: #8FB4DA; margin: 22px 0 2px;
}
.nav-drawer a {
  display: block; padding: 11px 0; min-height: 44px; color: #fff;
  font-size: var(--m-body); font-weight: 500;
  border-bottom: 1px solid rgba(255,255,255,.10);
}
/* The <=768px rule used to delete every sublabel, on the one screen where the
   labels are most ambiguous, because a wrapped bar had no room for them.
   A drawer has room. */
.nav-drawer a .nvd-d { display: block; font-size: var(--m-meta); font-weight: 400;
  color: #A9C4DE; margin-top: 1px; }
.nav-drawer a .nvd-tag { color: var(--primary-300); margin-left: 6px; }
.nav-drawer a:focus-visible { box-shadow: 0 0 0 3px rgba(255,255,255,.5); }
/* Standards and Pricing are ungrouped on desktop (flat links in the bar), so
   they are ungrouped here too — a rule instead of an invented heading. The old
   "Aligntra" <h2> grouped them into a category the desktop bar does not have. */
.nav-drawer .ungrouped { margin-top: 22px; border-top: 1px solid rgba(255,255,255,.10); }
.nav-drawer .btn-primary {
  margin-top: 24px; width: 100%; text-align: center; border: 0;
  background: #fff; color: var(--primary-900); border-bottom: 0;
}

/* ============== footer ============== */
.footer {
  background: var(--primary-900); color: #9FB6CC; padding: 60px 0 28px;
  border-top: 1px solid rgba(255,255,255,.10);  /* hairline so the CTA and footer read as two rooms of one dark close */
}
.fcols { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 28px; }
/* Eight of the nine standards have no page of their own. They are named here as
   plain text rather than as links that all resolve to /standards — five labels
   pointing at one URL read as a section and behave as a single link. */
.fnote { font-family: var(--mono); font-size: 12px; line-height: 1.75; color: #7B93AB;
  padding: 8px 0 0; margin-top: 4px; border-top: 1px solid rgba(255,255,255,.10); }
.fcols h3 {
  font-size: 12px; letter-spacing: .09em; text-transform: uppercase; color: #DCEBFA;
  font-weight: 600; margin-bottom: 16px;
}
.fcols a { display: block; font-size: var(--m-dense); color: #9FB6CC; padding: 5px 0; }
.fcols a:hover { color: #fff; }
.legal {
  display: flex; gap: 20px; flex-wrap: wrap; align-items: center; margin-top: 44px; padding-top: 22px;
  border-top: 1px solid rgba(255,255,255,.10); font-size: var(--m-meta); color: #7B93AB;
}
.legal a { color: #7B93AB; }
.legal a:hover { color: #DCEBFA; }
.legal .stamp { margin-left: auto; font-family: var(--mono); font-size: 12px; color: #8AA3BD; }  /* was 3.32:1 on navy */

/* ===========================================================================
   Mobile minimum — PLAN_MARKETING_V4_BUILD_PHASE1_2026-08-24.md S4.5.
   The locked mockups define NO responsive rules for nav/footer (S1 finding:
   their only two @media blocks target page-specific selectors that aren't
   even in this shared file). This section is new work, not a port. Scope is
   deliberately narrow: the nav row wraps instead of overflowing, and the
   4-column footer collapses to 2 then 1 column. Page-specific grid collapses
   (hero split, evidence band, demo strip, pricing cards, stats) live in each
   page's own {% block page_styles %}, per the same plan step.
   =========================================================================== */
@media (max-width: 768.98px) {
  /* The bar stays 68px and stays sticky. Both .links and .navr collapse behind
     the drawer trigger, so nothing wraps any more.

     This replaces the 21bdc8335 unpin (height:auto; position:relative). That
     existed only because the wrapped bar measured 223px at 390px wide, 26% of
     the screen, which is not something you want pinned. At 68px it is, and the
     cause is gone. */
  .links, .navr { display: none; }
  .nav-burger { display: inline-flex; }
  .fcols { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* Touch targets (WCAG 2.5.8 / 2.5.5). Measured on dev 2026-08-26 at 390px:
     footer links rendered 33px and 20px. Text size is unchanged, the tap area
     grows via padding, and the negative inline margin keeps the row rhythm.
     Drawer links carry their own 44px. */
  .fcols a { padding: 11px 0; margin: -3px 0; }
  .legal a { min-height: 44px; display: inline-flex; align-items: center; }
}

/* 769-959px: EVERY page scrolled horizontally. The nav collapses at 768 and is
   comfortable at >=960, but in between its single row is wider than the viewport
   and .navr (Sign in + Request a demo) pushed 36-116px past the edge. Measured
   across 15 widths: clean at <=768 and >=960, broken throughout the gap, worst
   at 820px - which is iPad portrait, along with 810 and 834.

   Pre-existing (+110px at the merge-base). Found by a three-width visual sweep;
   no test on this branch or on dev covers the tablet band at all - the mobile
   overflow lint checks 390px only.

   Deliberately narrower than the 768 block above: this wraps the nav row and
   nothing else. Collapsing the footer to two columns at 900px may well be right,
   but it is a design choice and this is a bug fix. */
@media (min-width: 769px) and (max-width: 959.98px) {
  /* height only. The phone block unpins the nav on purpose (a wrapped sticky bar
     eats 17% of a phone screen); copying that here silently made the nav
     non-sticky on every tablet, which the width sweep caught. A tablet has the
     room for a sticky bar and should keep it. */
  .nav { height: auto; }
  .nav .wrap { height: auto; flex-wrap: wrap; padding-top: 12px; padding-bottom: 12px; row-gap: 10px; }
  .links { order: 3; width: 100%; flex-wrap: wrap; gap: 10px 20px; margin-left: 0; }
}

/* 960-1099px: the SECOND half of the same bug, and the reason the block above
   did not close it.

   `@media (max-width: 959px)` is evaluated against the viewport width INCLUDING
   the classic scrollbar, but layout happens in the content width WITHOUT it. So
   at a 960px window the wrap rule has already switched off while the row only
   has ~945px to lay out in, and the single-row nav needs 1006px. Measured on
   dev 2026-08-30: 62px of horizontal overflow at a 960px window on EVERY v4
   page, clean again only at >=1024. The gap is real on small laptops and split
   windows, and the 769-959 block cannot see it.

   Fixed by tightening rather than wrapping: at these widths the row still fits
   on one line once the gaps come in, and wrapping a nav that fits is worse than
   closing it up. Measured requirement 1006px; this removes 98px (32 padding,
   58 links gap + margin, 8 navr gap), so the row needs 908px against 945px at
   the worst width in the band.

   Lock: tests/frontend/test_marketing_nav_tablet_band.py walks every width from
   769 to 1099 and fails on any width no rule covers. That is the check that was
   missing - the mobile overflow lint tests 390px only, which is why this half
   shipped. */
/* THE SCROLLBAR IS 15px AND IT IS NOT OPTIONAL. Read this before touching the
   numbers, because this band has now been wrong twice for the same reason.

   A flex row short of space does not overflow — it wraps its most shrinkable
   item. `Sign in` is that item. So every previous budget here was checked against
   `scrollWidth == clientWidth`, which stayed 0, while the bar visibly broke.

   The budget, measured in real Chrome at 960px (2026-08-31):
     window 960  ->  layout 945  (a classic scrollbar takes 15)
     minus this block's horizontal padding, twice
     the row itself needs 913.2 (brand 100.3 + links 507.1 + navr 221.8
                                 + two 36px wrap gaps + the 12px links margin)
   At 28px of padding that leaves 889 against 913.2 and it wraps. At 20px it
   leaves 905, and gap 18 -> 16 frees the last 10px across five gaps.

   Padding is the lever, and it costs something: `.wrap` is 44px everywhere else,
   so the nav's content already sat 16px inboard of the page content below it in
   this band and now sits 24px inboard. That is the price of one row at 960px,
   and 960 is not an exotic width — it is a maximised window on a 1440x900 screen
   at 150% scaling. The alternatives were worse: `margin-left` is the brand's own
   zone, widened deliberately by REVIEW_MARKETING_V4_NAV_2026-08-26 so the
   wordmark stops reading as the leftmost menu item, `.navr`'s gap is what stops
   `Sign in` reading as part of the CTA, and letting the row wrap to two lines at
   the single most common laptop width is not a fix.

   Locks: tests/e2e/browser_unit/test_marketing_v4_nav_fits_every_width.py renders
   the page and walks every width asserting nothing WRAPS — with the 15px
   scrollbar EMULATED, because headless Chromium uses overlay scrollbars and
   measures 15px more room than a real user has. That blind spot is what let the
   previous attempt ship green. tests/frontend/test_marketing_nav_tablet_band.py
   holds the static side: coverage, and that the bands actually meet. */
@media (min-width: 960px) and (max-width: 1099.98px) {
  .nav .wrap { padding-left: 20px; padding-right: 20px; }
  .links { gap: 16px; margin-left: 12px; }
  .navr { gap: 18px; }
}

@media (max-width: 480px) {
  .fcols { grid-template-columns: 1fr; }
  .legal { flex-direction: column; align-items: flex-start; gap: 10px; }
  .legal .stamp { margin-left: 0; }
}

/* Phone nav (S5 QC fix, 2026-08-24): the three-button .navr is wider than a
   390px viewport. Below 540px the secondary (Watch the demo) leaves the nav
   bar - the same trim the homepage ships at every width (VCP-approved); the
   watch path stays reachable in page content and the footer. Found by the
   route-rendered 390 capture; the rendered-twin harnesses had measured a
   narrower nav (hardened same day to assert this rule directly). */
@media (max-width: 540px) {
  .navr { gap: 10px; }
  /* `.navr .btn-secondary { display: none }` removed: the only thing that could
     render one is the nav_secondary block, whose default has been empty
     sitewide since the 2026-08-25 VCP round and which no template overrides
     with content. */
}

/* Skip link (WCAG 2.4.1) - visually hidden until keyboard focus. Carried
   from the v3 base at the phase-2 flip (2026-08-25); first focusable
   element on every marketing page, targets <main id="main-content">. */
.skip-link { position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--primary-600); color: #fff; padding: 10px 18px;
  border-radius: 0 0 8px 0; font-weight: 600; font-size: 14px; }
.skip-link:focus { left: 0; }

/* Reduced-motion blanket (WCAG 2.3.3) - carried from the v3 base at the
   phase-2 flip. Catches every per-page hover transform without each page
   opting in. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  /* Motion vocabulary (2026-08-25) hover states neutralized outright, not
     just sped up — a 2px/1px translate still reads as motion even at
     0.01ms duration. */
  .card:hover, .btn-primary:hover,
  .links > a:hover::after, .links > a:focus-visible::after,
  .nvd-t:hover::after, .nvd-t:focus-visible::after {
    transform: none !important;
  }
  /* `.nvd.open .nvd-t .car` is deliberately NOT here. That rotation is the
     disclosure's open/closed STATE, not decoration — removing it would delete
     information. The global rule above already collapses its transition to
     0.01ms, so it snaps rather than animates. */
}

/* ============== industry pages: the two genuinely shared sections ==============
   .ind-find (the worked-finding band) and .ind-cross (the also-serving rail) are
   the only sections the four industry templates hold in common. Their nine rules
   were byte-identical copies in all four files — 36 copies of 9 rules — which is
   the same per-page-duplication debt this layer exists to retire. Hoisted here;
   a change now lands on all four at once instead of three-out-of-four. Every
   other section on those pages is genuinely bespoke and stays in its template. */
.ind-find { background: linear-gradient(170deg, var(--primary-800), var(--primary-900));
            padding: var(--sp-lg) 0; }
.ind-find .in { max-width: 900px; margin: 0 auto; padding: 0 44px; }
.ind-find .eyebrow { color: var(--primary-300); }
.ind-find h2 { color: #fff; font-size: 28px; letter-spacing: -0.025em; font-weight: 600; }
.ind-find .sub { color: #A9C4DE; font-size: 17px; margin-top: 10px; max-width: 60ch; line-height: 1.6; }
.ind-cross { padding: var(--sp-xs) 0; background: #fff; text-align: center;
             font-size: 14px; color: var(--gray-600); }
.ind-cross .lbl { font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase;
                  color: var(--gray-500); margin-right: 6px; }
.ind-cross a { color: var(--gray-700); font-weight: 500; }
.ind-cross a:hover { color: var(--primary-600); }

/* ============== nav dropdowns (2026-08-25) ==============
   Disclosure groups in .links — trigger typography mirrors .links a
   exactly; panels are white cards on the shared tokens.

   No `.nav` rule lives here. This section used to re-declare
   `position: sticky; top: 0; z-index: 50` for the stacking context, but the
   base .nav rule above already carries all three. Being LATER in the file than
   the <=768px block, the duplicate silently beat that block's
   `position: relative; top: auto` — media queries add no specificity, so
   source order decided it, and the phone-unpin fix of 21bdc8335 never took
   effect. Do not re-add a `.nav` declaration below the media queries. */
/* `display: flex` and `line-height: inherit` are the alignment pair — do not drop
   either. The trigger is a <button> and the flat links (Services, Standards,
   Pricing) are <a>. The reset below zeroes background, border, padding and
   font-family but used to leave line-height at the UA's `normal`, so the button's
   box was 19px against the anchors' 24.8px. Both underlines are drawn at
   `bottom: -6px` off their own box, so they landed 2.8px apart and the labels sat
   1px apart: the bar visibly split into "things that drop down" and "things that
   do not". `line-height: inherit` finishes the reset and `display: flex` lets .nvd
   stretch to the row the way the anchors already do. Measured at 960/1024/1280/
   1400/1600px the underline delta is 0.00px; the panel hangs 0.76px higher.
   Fixing it by nudging one ::after offset instead would leave the two boxes
   different and break again the next time the type scale moves. */
.nvd { position: relative; display: flex; }
.nvd-t { display: inline-flex; align-items: center; gap: 5px; background: none;
  border: 0; padding: 0; cursor: pointer; font-family: inherit; line-height: inherit;
  font-size: var(--m-dense); color: #DCEBFA; font-weight: 500; position: relative; }
.nvd-t:hover, .nvd-t.on { color: #fff; }

/* The active item needs a marker that is not brightness. Rest ink is now #DCEBFA,
   so `.on`'s white is a small step — too small to read as "you are here" on a bare
   wayfinding label. This reuses the underline the flat Pricing link already uses
   for hover, made persistent for `.on`, rather than inventing a second marker.
   Safe on .nvd-t specifically because the only buttons in .links ARE the triggers
   (the panel rows are anchors, which is why the sibling rule below is scoped to
   DIRECT children). */
.nvd-t::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -6px; height: 2px;
  background: rgba(255,255,255,.45); transform: scaleX(0); transform-origin: left;
  transition: transform .16s ease;
}
.nvd-t:hover::after, .nvd-t:focus-visible::after, .nvd-t.on::after { transform: scaleX(1); }
.nvd-t.on::after { background: #fff; }
.nvd-t .car { transition: transform .15s ease; }
.nvd.open .nvd-t .car { transform: rotate(180deg); }
/* Fixed width, not min-width. With `min-width` + `white-space: nowrap` each
   panel's width was a function of its longest string, so the four adjacent
   triggers opened four differently-sized cards. Two widths now: the default,
   and --wide for Industries, whose rows carry mono standard lists. Rows wrap
   instead of forcing the card open. (A dead `min-width: 236px` lived here too,
   overridden four lines later by 300px.)

   Three widths since 2026-08-30: the default, --duo for Product's two-column
   panel, and --wide back for Industries. `--wide` had been removed with the
   Industries panel that was its only consumer, and that door returned on
   2026-08-28; the sector rows now name their standards, and the medical-device
   line (four names) measures 272.1px in IBM Plex Sans against the 278px the
   320px panel leaves for text. Six pixels is not slack. Measured at 13px/500
   for the same string: Arial, Helvetica, Liberation Sans and the generic
   `sans-serif` all land at 278.9px, DejaVu Sans at 293.9px and Verdana at
   305.0px, so the row wraps on every machine that falls off the webfont onto
   the end of the declared stack. At 380px there is 338px and all of them
   fit. */
.nvd-p { display: none; position: absolute; top: calc(100% + 14px); left: -14px;
  width: 320px; background: #fff; border: 1px solid var(--gray-200);
  border-radius: 10px; box-shadow: var(--shadow-lg); padding: 8px; z-index: 120; }
/* Product's two-column panel (owner-ratified 2026-08-27). A third fixed width,
   not min-width:auto — see the .nvd-p comment above for why the widths are
   fixed. 620px = 2 x 292px columns + 8px panel padding x2 + 20px gutter.
   Product is trigger 1, so the panel opens at x~213 and ends at ~833 inside an
   1112px content box; the shared left:-14px still works, no edge detection. */
.nvd-p--duo { width: 620px; padding: 14px 8px 8px; }
/* Industries. 380px leaves 340px for the row text, which fits the longest
   sector line with room to spare instead of by a hair. */
.nvd-p--wide { width: 380px; }
.nvd-duo { display: grid; grid-template-columns: 1fr 1fr; gap: 0 20px; }
/* Column head reuses the machine-metadata tier the site already has. Padded to
   12px so it aligns with the row LABEL, not with the row's hover rail. */
.nvd-colhead { font-family: var(--mono); font-size: 11px; font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--gray-500); padding: 0 12px 7px;
  border-bottom: 1px solid var(--gray-200); margin: 0 0 6px; }
/* The lead row keeps the full two-line treatment; the capability rows beneath
   are label-only. Two densities in one column is the point — the lead is a
   destination, the rows under it are an index into it. */
.nvd-lead { border-bottom: 1px solid var(--gray-200); padding-bottom: 6px; margin-bottom: 6px; }
.nvd-p a.compact { padding: 6px 12px; }
.nvd-p a.compact b { font-weight: 500; color: var(--gray-700); }
.nvd-p a.compact:hover b { color: var(--gray-900); }
/* The capability rows are an index INTO the lead above them, so they sit
   indented under a hairline rail (owner request 2026-09-01): the rail says
   "contents of", where four flush rows read as four more destinations. */
.nvd-sub { margin-left: 20px; border-left: 2px solid var(--gray-200); }
/* SOP Builder is a different PRODUCT, not another capability, so it spans the
   foot under a full-width rule instead of sitting in either column. */
.nvd-duo-foot { grid-column: 1 / -1; border-top: 1px solid var(--gray-200);
  margin-top: 8px; padding-top: 6px; }
/* Single-column panel foot (Industries → Consulting), 2026-08-28. Same rule
   as the duo foot without the grid span; the colhead inside it drops its own
   underline so the foot carries one rule, not two. */
.nvd-foot { border-top: 1px solid var(--gray-200); margin-top: 8px; padding-top: 6px; }
.nvd-foot .nvd-colhead { border-bottom: none; padding-bottom: 4px; margin-bottom: 2px; }
.nvd.open .nvd-p { display: block; }
/* .nvd-p is a descendant of .links, so the white `.links a` rule above paints
   these too — right for the phone accordion, wrong on this white desktop card.
   Every row currently wraps its text in <b>/.nvd-d, which set their own ink, so
   nothing renders white-on-white today; this reset means that stays true if a
   row is ever added with bare text. */
/* Rail-side corners are SQUARE: an accent rail under a rounded corner clips
   crescents (.claude/rules/frontend.md). The right side keeps its 7px. */
.nvd-p a { display: block; position: relative; padding: 9px 12px; border-radius: 0 7px 7px 0;
  transition: background .12s ease; color: var(--gray-800); }
/* Hover rail. A row IS the card inside a panel, so this is the row's own
   accent, not a second rail inside a railed container. Inset top and bottom so
   it never runs into the row's rounded right edge. The background tint alone
   was the whole hover signal, and a tint has no left edge to scan along. */
.nvd-p a::before {
  content: ''; position: absolute; left: 0; top: 7px; bottom: 7px; width: 2px;
  background: var(--primary-600); opacity: 0; transition: opacity .12s ease;
}
.nvd-p a:hover::before, .nvd-p a:focus-visible::before { opacity: 1; }
.nvd-p a b { display: flex; align-items: baseline; gap: 8px; font-size: var(--m-dense); color: var(--gray-800); font-weight: 600; }
.nvd-p a b .nvd-tag { margin-left: auto; }  /* status tag sits at the row's far edge (VCP: crowded the label) */
/* ...except in the duo foot, which spans 620px. `margin-left: auto` was tuned
   against a 320px panel; across the full width it parks the tag 408px from the
   end of its own label (measured), so it stops reading as attached to it. The
   original VCP finding was crowding at 320px — this keeps that fix where it
   applies and undoes the overcorrection where it does not. */
.nvd-duo-foot a b .nvd-tag { margin-left: 8px; }
.nvd-p a .nvd-d { display: block; font-size: var(--m-meta); color: var(--gray-500); margin-top: 2px; }
.nvd-p a .nvd-d.mono { font-family: var(--mono); font-size: var(--m-meta); letter-spacing: .02em; }
/* --gray-50 (#f9fafb) on #fff is 1.045:1 — every row in every panel was inert
   on hover. --primary-50 is a hue shift as well as a lightness one, so it
   registers where a near-white grey did not. */
.nvd-p a:hover { background: var(--primary-50); }
.nvd-p a:hover b { color: var(--gray-900); }
.nvd-tag { font-family: var(--mono); font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--primary-600); margin-left: 6px; }

/* No phone rules for .nvd-p: below 768px .links is display:none and the drawer
   serves that content instead, so the old inline-accordion block (a transparent
   panel, inverted ink, suppressed sublabels) is gone. It only ever existed as a
   way to survive without a drawer. */


/* ==========================================================================
   Scroll reveal — PLAN_MARKETING_MOTION_V4_2026-08-29.md S1 (2026-08-29)

   The v4 estate had motion in exactly one place: the homepage `.hero-loop`.
   Measured across all 27 templates, the other 23 carried zero @keyframes,
   zero animation: and zero transition: declarations - no reveals, no hover
   states. This is the shared primitive that lets the rest of the estate
   acknowledge the hero rather than sitting dead beside it.

   SAFETY CONTRACT - content is VISIBLE by default.
   The hidden state is gated on `html.rv-on`, which only ever gets added by
   script, and only when motion is not reduced. A blocked script, a throw, a
   browser without IntersectionObserver, or a reduce-motion preference all
   leave every page fully rendered. A watchdog in _base.html removes the class
   if the observer never wires up. NEVER author the inverse (hidden in CSS,
   revealed by JS) - that turns one JS error into a blank marketing site.

   Reduced motion is additionally covered by the blanket rule above (:442),
   but the gate is what makes it safe, not the blanket.
   ========================================================================== */
html.rv-on [data-rv] {
  opacity: 0;
  transform: translateY(10px);
}
html.rv-on [data-rv].rv-in {
  opacity: 1;
  transform: none;
  transition: opacity 420ms cubic-bezier(.22, .75, .25, 1),
              transform 420ms cubic-bezier(.22, .75, .25, 1);
  /* Siblings entering together stagger; --rv-i is capped at 6 by the wiring
     so a 20-card grid does not trail 1.4s behind its own first row. */
  transition-delay: calc(var(--rv-i, 0) * 70ms);
}

/* Printing fires no scroll, resize or pageshow, so the sweep above never runs
   and whatever the reader had not yet scrolled past prints at opacity 0.
   Measured on /how-it-works before this rule: a headless print at 1440x900
   with no prior scroll left 9 of 15 targets hidden — the whole of act 02, act
   03, the outputs grid and the closing band came out as empty slabs. A
   prospect printing a method page to circulate internally got blank sections.

   This only ever un-hides what the reveal itself hid, inside @media print, so
   it cannot affect screen rendering. It belongs here rather than on any one
   page: every v4 template inherits the reveal, so every one of them had it. */
@media print {
  html.rv-on [data-rv] { opacity: 1 !important; transform: none !important; }
}
