/* =========================================================================
   The Growth Transfer — design system
   -------------------------------------------------------------------------
   One stylesheet, no build step, no dependencies. Hand authored.

   The system is two surfaces, not two themes. `color-scheme: light` is
   declared once and never revisited; a dark passage is a `.band`, which is a
   markup decision, not a media query. Every component that can sit on both
   surfaces carries its `.band` override immediately beneath it, in the same
   block, so the two can never drift apart.

   Identity: ink / paper / gold. Squared corners everywhere. No shadows, no
   gradients, no rounded anything. One gold accent per composition.

   Contract: docs/BUILD-CONTRACT.md. Reference: docs/BRAND-SYSTEM.md.
   If this file and BRAND-SYSTEM.md disagree, this file is right.
   ========================================================================= */


/* =========================================================================
   1. SELF-HOSTED TYPE
   -------------------------------------------------------------------------
   These three families were previously pulled from fonts.googleapis.com: a
   render-blocking third-party request on the critical path and a privacy
   exposure for EU visitors. Now they are latin-subset woff2 files in fonts/.

   Archivo and Source Serif 4 are variable fonts, so one file covers the whole
   weight range each is used at. IBM Plex Mono ships as three static cuts
   because the mono is only ever used at 400, 500 and 600 and three subset
   files are smaller than one variable one.

   All three are SIL Open Font License 1.1. The licence text ships in fonts/,
   which is what the OFL requires of anyone redistributing the binaries.

   NOTE for whoever maintains fonts/: archivo.woff2 and source-serif.woff2
   MUST be the variable cuts. A static 400-weight file behind a `font-weight:
   400 700` descriptor renders every heading at 400 with no synthesis, which
   reads as "the bold went missing" rather than as an error.
   ========================================================================= */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/archivo.woff2") format("woff2");
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/plex-mono-400.woff2") format("woff2");
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../fonts/plex-mono-500.woff2") format("woff2");
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/plex-mono-600.woff2") format("woff2");
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("../fonts/source-serif.woff2") format("woff2");
}


/* =========================================================================
   2. TOKENS
   -------------------------------------------------------------------------
   Contrast measured, not eyeballed (scripts in the build scratch area, WCAG
   2.1 relative-luminance formula, alpha values composited against their real
   ground before measuring):

     --ink-92 on --paper      13.32:1   AA
     --ink-72 on --paper       6.87:1   AA   <- default body text
     --ink-66 on --paper       5.59:1   AA   <- meta/caption ramp
     --ink-66 on --paper-alt   5.47:1   AA
     --ink-66 on --tile        5.28:1   AA
     --paper-88 on --ink      12.68:1   AA
     --paper-60 on --ink       6.48:1   AA
     --gold-ink on --paper     5.24:1   AA   <- the darkened twin
     --gold-ink on --tile      4.59:1   AA   (the tightest ground it sits on)
     --gold on --ink           7.35:1   AA
     --gold on --paper         2.22:1   BANNED, and why the twin exists

   Non-text (WCAG 1.4.11, 3:1 floor). Hairline dividers are decorative and may
   sit below it; the boundary of a CONTROL may not, so form fields and ghost
   buttons take their own token rather than the hairline:

     --control on --paper      3.36:1   AA (input + ghost borders on paper)
     --control-ink on --ink    3.79:1   AA (input + ghost borders on ink)
     --line on --paper         1.34:1   decorative divider only
     --line-paper on --ink     1.57:1   decorative divider only

   The old body colour rgba(23,21,18,.45) measured 2.89:1 and was the dominant
   text colour on the site. It is banned. Do not reintroduce it.

   The meta ramp was rgba(23,21,18,.55) (3.91:1) until the pre-ship audit:
   contract section 1 named it, but section 4 makes 4.5:1 a blocking floor and
   every consumer of it (.label, .figure-cap, .spec-k, .gauge-cap, .debt-kicker,
   .field label) is 10-11px mono, which is not WCAG "large text". Section 4
   wins over section 1; the token is now .66.
   ========================================================================= */
:root{
  color-scheme: light;
  --ink:#171512;
  --ink-92:rgba(23,21,18,.92);   /* headings on paper */
  --ink-72:rgba(23,21,18,.72);   /* body on paper — AA (≈6:1) */
  --ink-66:rgba(23,21,18,.66);   /* meta/captions on paper — AA at 10-11px mono */
  --paper:#f5f2ea;
  --paper-alt:#f0ece2;           /* alternating section fill */
  --tile:#e8e3d9;                /* photo/diagram tile fill */
  --paper-88:rgba(245,242,234,.88); /* body on ink — AA */
  --paper-60:rgba(245,242,234,.60); /* meta on ink */
  --line:rgba(23,21,18,.14);     /* hairlines on paper — decorative dividers only */
  --line-paper:rgba(245,242,234,.16); /* hairlines on ink — decorative dividers only */
  --control:rgba(23,21,18,.50);  /* control boundaries on paper — 3.36:1, WCAG 1.4.11 */
  --control-ink:rgba(245,242,234,.42); /* control boundaries on ink — 3.79:1 */
  --gold:#c2a05a;                /* accent on ink ONLY */
  --gold-ink:#7d602b;            /* accent on paper — darkened twin; measures 5.24:1 on --paper */
  --gold-soft:rgba(194,160,90,.5);
  --sans:'Archivo',-apple-system,system-ui,sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,'SF Mono',monospace;
  --serif:'Source Serif 4',Georgia,serif;
}

/* -------------------------------------------------------------------------
   Surface layer.

   Every component below paints with these six names instead of reaching for
   a raw colour. `.band` and `.section--alt` reassign them, so a component
   dropped onto an ink ground recolours itself without knowing it moved. The
   explicit `.band .x` overrides that follow each component exist for the
   cases this cannot cover: inverted buttons, grid-cell fills, focus rings,
   and anywhere `.band a` (0,1,1) would out-specify a component class (0,1,0).
   ------------------------------------------------------------------------- */
:root{
  --ground: var(--paper);        /* the section's own background */
  --fill: var(--paper);          /* tiles, cells and cards sitting on it */
  --fill-alt: var(--paper-alt);  /* the alternate cell in a zebra run */
  --text-strong: var(--ink-92);  /* headings */
  --text: var(--ink-72);         /* body */
  --text-dim: var(--ink-66);     /* meta, captions, mono labels */
  --hairline: var(--line);       /* decorative dividers */
  --control-line: var(--control);/* interactive boundaries (inputs, ghost buttons) */
  --accent: var(--gold-ink);
  --focus: var(--gold-ink);
  --ease: cubic-bezier(0.22,0.61,0.36,1);
}


/* =========================================================================
   3. RESET + BASE
   ========================================================================= */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  /* the sticky nav is ~72px tall; 90px keeps an anchored heading clear of it */
  scroll-padding-top: 90px;
  -webkit-text-size-adjust: 100%;
}

body {
  background: var(--paper);
  color: var(--ink-72);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.7;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  min-height: 100vh;
  min-height: 100dvh;
}

/* A page whose content is shorter than the viewport still paints `body` under
   the footer. On a page that ends in the ink footer that leaves a cream band
   below it, reading as though the footer detached. 404.html is the only such
   page today; the class exists so the next short page has one. */
body.page-ink { background: var(--ink); }

img, svg, video { max-width: 100%; }
img { height: auto; display: block; border-radius: 0; }
svg { display: block; }

button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: none; cursor: pointer; }

/* squared corners are the identity, not a default to be overridden */
* { border-radius: 0; }

/* `main` is focusable via the skip link (tabindex="-1"). It deliberately does
   NOT get `outline: none` here: landing a keyboard user on an unmarked region
   is worse than one extra ring, and `outline: none` appears nowhere in this
   file by contract. The global :focus-visible rule covers it. */


/* =========================================================================
   4. TYPOGRAPHY
   ========================================================================= */
h1, h2, h3, h4 {
  font-family: var(--sans);
  color: var(--text-strong);
  letter-spacing: -0.02em;
  font-weight: 600;
}
h1 { font-size: clamp(38px, 6vw, 64px); line-height: 1.04; }
h2 { font-size: clamp(28px, 4vw, 40px); line-height: 1.08; }
h3 { font-size: 22px; line-height: 1.25; letter-spacing: -0.01em; }
h4 { font-size: 18px; line-height: 1.3; letter-spacing: -0.01em; }
.band h1, .band h2, .band h3, .band h4 { color: var(--paper); }

/* Outline-vs-visual decoupling: a card title that belongs at h2/h3 in the
   document outline but should not shout at h2/h3 size. Component classes that
   restyle a promoted heading (.stat-label, .stage-name) are declared after
   this rule so they win on source order at equal specificity. */
.h-sub {
  font-size: 17px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--text-strong);
}

/* ---- rag control, globally ----------------------------------------------
   DESIGN-PRINCIPLES section 1: a headline may not strand a lone word at ANY
   width, and the structural fix is `text-wrap: balance` rather than a hand
   placed break, because balance minimises the longest line and therefore
   holds at widths nobody screenshotted.

   Before this block the whole stylesheet carried exactly two text-wrap
   declarations: `.hero h1 .hero-line` and `.hero-sub`. Every other h1 and h2
   on all seven pages wrapped greedily, which is how methodology stranded
   "machine", 404 stranded "exist." and the funnel intro stranded "you." — the
   punch word of the product — at 390, 430 and 768 simultaneously.

   balance is capped by the engine at a handful of lines (Chrome stops at 6),
   so it is spent on display type and on the short utility classes that behave
   like display type. Everything that runs to real paragraph length takes
   `pretty` instead, which only reserves the last line and therefore cannot
   orphan a final word.

   The funnel selectors are here rather than in score/index.html's inline
   sheet on purpose. That sheet loads AFTER this file, but it sets no
   text-wrap of its own, so there is nothing to lose the cascade to and the
   rule belongs with every other page's. */
h1, h2, h3, h4,
.h-sub, .lede, .stat-num, .stage-name, .faq-q, .pull, .gauge-cap,
.fn-intro h1, .fn-gate h1, .fn-qright h1, .fn-close h2, .fn-block > h2,
.fn-dive h3, .fn-fcard h3, .fn-qleft-sub {
  text-wrap: balance;
}
p, li, dd,
.note, .fine, .spec-v, .stat-note, .stage-note, .debt-note, .faq-a,
.tquote, .prose-serif, .fn-serif, .fn-dive-body, .fn-opt-text {
  text-wrap: pretty;
}

/* The trademark glyph in display type. `&trade;` inherits the heading's font
   size, so at the 64px h1 ceiling it renders as roughly 40px of "TM" bolted
   to the side of a word. At 0.42em on a raised baseline it reads as the mark
   it is. Wrap it in the markup: `Score<span class="tm">&trade;</span>`. */
.tm {
  font-size: 0.42em;
  vertical-align: 0.5em;
  letter-spacing: 0;
  font-weight: 500;
  line-height: 0;
}

p { color: var(--text); }
strong, b { font-weight: 600; color: var(--text-strong); }
.band strong, .band b { color: var(--paper); }

/* ---- default vertical rhythm --------------------------------------------
   The reset zeroes every margin. Without a default rhythm a page agent has
   to reach for inline `style="margin-top:..."` on every heading, which the
   markup rules forbid, so the spacing between a kicker, its heading and the
   paragraph under it lives here.

   Adjacent-sibling only, and every component that already manages its own
   spacing (flex `gap`, an explicit margin-bottom, or a grid row where a
   top margin would offset one column against the other) is neutralised
   immediately below. Two paragraphs in a row still get their gap. */
.kicker + h1, .kicker + h2, .kicker + h3, .kicker + .h-sub { margin-top: 16px; }
h1 + p, h2 + p, h3 + p, .h-sub + p,
h1 + .lede, h2 + .lede, h3 + .lede { margin-top: 18px; }
p + p { margin-top: 18px; }

/* components that space themselves */
.card > p + p,
.stat .stat-label + .stat-note,
.stage .stage-name + .stage-note,
.faq-q + .faq-a,
.step-body .h-sub + p, .step-body h3 + p,
.spec-row > p + p,
.footer-bottom p + p { margin-top: 0; }

/* the sentence under a heading: one size up from body, never a second heading */
.lede {
  font-size: clamp(17px, 2vw, 19px);
  line-height: 1.6;
  color: var(--text);
  max-width: 58ch;
}
.hero .lede, .band .lede { color: var(--paper-88); }

/* .note and .fine are small SANS prose, so they take the 6.9:1 body ramp, not
   the --ink-66 meta ramp. Both ramps clear AA; the split is a hierarchy
   decision, not a contrast one. */
.note { font-size: 14px; line-height: 1.6; color: var(--text); }
.band .note { color: var(--paper-88); }

.fine { font-size: 13px; line-height: 1.6; color: var(--text); }
.band .fine { color: var(--paper-88); }

/* ---- mono labels ---------------------------------------------------------
   .kicker sits above a heading. .label is the same voice at cell/table scale.
   Both carry 500 weight: at 10-11px the 400 cut goes thin enough that the
   --ink-66 ramp stops being comfortably readable. */
.kicker {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--accent);
}
.band .kicker { color: var(--gold); }

/* A kicker for a composition whose one gold accent is somewhere else. The home
   hero colours its punch line in gold, and a gold kicker above a gold headline
   reads as two accents fighting rather than as a label introducing a headline.
   Takes the meta ramp: --ink-66 on paper (5.59:1), --paper-60 on ink (6.48:1).
   Use it only when the accent has genuinely moved, never to quiet a kicker
   that is still the composition's only gold.

   Each ground that carries an explicit `.kicker` colour override also needs an
   explicit quiet twin at matching specificity, declared after it — see
   `.hero .kicker--quiet` in the hero block. */
.kicker--quiet { color: var(--text-dim); }
.band .kicker--quiet { color: var(--paper-60); }

.label {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--text-dim);
}
.band .label { color: var(--paper-60); }
.label--accent { color: var(--accent); }
.band .label--accent { color: var(--gold); }

.mono { font-family: var(--mono); }

/* ---- serif -------------------------------------------------------------
   Source Serif earns its download on reflective passages only: pull quotes,
   the "why a loop" prose, testimonial quotes, and the funnel report prose.
   Body copy stays in the sans. */
.prose-serif {
  font-family: var(--serif);
  font-weight: 400;
  font-size: 19px;
  line-height: 1.7;
  color: var(--text);
}
.prose-serif p + p { margin-top: 20px; }
.band .prose-serif { color: var(--paper-88); }

.pull {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(22px, 3vw, 30px);
  line-height: 1.42;
  color: var(--text-strong);
  border-left: 2px solid var(--accent);
  padding-left: 28px;
  max-width: 42ch;
}
.band .pull { color: var(--paper); border-left-color: var(--gold); }

/* ---- rules --------------------------------------------------------------- */
.rule-gold {
  border: none;
  border-top: 1px solid var(--accent);
  width: 56px;
  margin: 0;
}
.band .rule-gold { border-top-color: var(--gold); }
.rule-line { border: none; border-top: 1px solid var(--hairline); margin: 0; }

/* ---- links --------------------------------------------------------------- */
a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--ink); }
.band a { color: var(--gold); }
.band a:hover { color: var(--paper); }

/* a link that reads as body text and underlines on the hairline */
.link-quiet {
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px solid var(--hairline);
  padding-bottom: 1px;
}
.link-quiet:hover { color: var(--text-strong); border-bottom-color: var(--accent); }
.band .link-quiet { color: var(--paper-88); border-bottom-color: var(--line-paper); }
.band .link-quiet:hover { color: var(--paper); border-bottom-color: var(--gold); }


/* =========================================================================
   5. ACCESSIBILITY PRIMITIVES
   ========================================================================= */
/* NEVER outline:none. The keyboard path is the one nobody tests and everybody
   inherits. Gold is 2.22:1 on paper, so the light ground takes the darkened
   twin (5.24:1) and ink grounds take paper (16.29:1). */
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }
.band :focus-visible, .hero :focus-visible, .footer :focus-visible { outline-color: var(--paper); }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 14px 22px;
  border: 2px solid var(--gold);
}
.skip-link:focus { left: 8px; top: 8px; color: var(--paper); }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Safari drops list semantics from a list-style:none list, which is why every
   such list in the markup carries role="list". This is the styling half. */
[role="list"], .list-plain { list-style: none; }


/* =========================================================================
   6. CONTAINERS + SECTIONS + SURFACES
   ========================================================================= */
.wrap      { max-width: 1040px; margin: 0 auto; padding: 0 28px; }
.wrap-wide { max-width: 1200px; margin: 0 auto; padding: 0 28px; }
/* `.reading` centres itself. It was `max-width` only, and `.center .reading`
   auto-margined it, so the ONE page that uses a narrow measure without
   centring its text — privacy — ran its entire 5,113px policy column flush
   left in a 1040px wrap with 320px of blank paper down the right edge at
   1440. A narrow measure is a typographic decision, not a left-alignment
   one; the two were coupled by accident. */
.reading   { max-width: 720px; margin-inline: auto; }
.measure   { max-width: 62ch; }
.center    { text-align: center; }
.center .rule-gold, .center .depmap, .center .gauge { margin-left: auto; margin-right: auto; }
.center .lede, .center .measure, .center .reading { margin-left: auto; margin-right: auto; }

.section { padding: 88px 0; background: var(--ground); color: var(--text); }
.section--tight { padding: 56px 0; }
.section--line { border-bottom: 1px solid var(--hairline); }

/* the second surface: same paper family, one step down, for alternation */
.section--alt {
  --ground: var(--paper-alt);
  --fill: var(--paper);   /* cells lift off the alt ground rather than vanish */
  /* the zebra twin steps DOWN from --fill rather than up, because --paper-alt
     is this section's own ground and a cell painted in it would disappear */
  --fill-alt: var(--tile);
}

/* -------------------------------------------------------------------------
   .band — the ink ground.

   Reassigning the surface tokens is what recolours the twenty components
   inside it. The explicit `.band .x` rules elsewhere in this file handle the
   three things token reassignment cannot: inverted buttons, the focus ring,
   and any place `.band a` (specificity 0,1,1) would beat a bare component
   class (0,1,0). That last one is not hypothetical — it is exactly how a gold
   button inside a dark band ends up with 1.07:1 label text.
   ------------------------------------------------------------------------- */
.band {
  --ground: var(--ink);
  --fill: var(--ink);
  --text-strong: var(--paper);
  --text: var(--paper-88);
  --text-dim: var(--paper-60);
  --hairline: var(--line-paper);
  --control-line: var(--control-ink);
  --accent: var(--gold);
  --focus: var(--paper);
  --fill-alt: rgba(245,242,234,0.05);
  background: var(--ink);
  color: var(--paper-88);
}
.band p { color: var(--paper-88); }

/* two-column editorial split: copy beside a diagram, gauge pair or photo */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 64px;
  align-items: center;
}
.split--top { align-items: start; }
/* the hairline that separates the two halves, matching the grid system */
.split--rule > :last-child {
  border-left: 1px solid var(--hairline);
  padding-left: 56px;
}

/* `.split--panel` gives the second column a FLOOR. Four consecutive phase
   sections on methodology each ran their right-hand Outputs list out of
   content about 60% up, leaving four identical empty rectangles bottom right;
   an unbounded column cannot end, so it just stops. A bounded hairline panel
   ends on purpose.

   `align-self: start`, NOT `stretch`. Stretch was the original spelling and
   it traded one empty rectangle for another: at 1440 a panel holding four
   short list items drew to 355px for 210px of content, so about 130px of
   blank paper sat INSIDE the drawn box, four times down the page. A panel
   that hugs its content is a floor that ends on purpose; a stretched one is
   the same unfinished column with a border on it. It was already correct on
   the phone, where the column stacks and stretch has nothing to stretch to.

   (A `.split--flip` visual-order modifier lived here too, for alternating the
   run. Zero uses, and unsafe as written — no order reset at the single-column
   breakpoint, so a collapsed column would have put the second column above
   its own heading. Deleted at fix time rather than fixed, because its own
   comment ruled it out for the one run it was written for: the Outputs panel
   reads as a consequence of the phase description beside it, not as its
   peer.) */
.split--panel > :last-child {
  border: 1px solid var(--hairline);
  background: var(--fill);
  padding: 30px 28px;
  align-self: start;
}

/* a stack of blocks with consistent rhythm, for prose sections */
.stack > * + * { margin-top: 20px; }
.stack-lg > * + * { margin-top: 32px; }

/* the standard gap between a section's intro block and its content */
.section-head { margin-bottom: 56px; }
.section-head .kicker { margin-bottom: 18px; }
.section-head h2 + .lede, .section-head h1 + .lede { margin-top: 18px; }
.section-head .rule-gold { margin-top: 24px; }


/* =========================================================================
   7. NAV
   -------------------------------------------------------------------------
   Paper ground, bottom hairline, sticky. `.nav--stuck` is added by site.js
   past scrollY > 60 and does two things: a translucent backdrop so long-form
   copy scrolling under it stays legible, and a stronger hairline so the bar
   reads as a separate plane rather than as the top of the page.

   THE STICKY LIVES ON `header`, NOT ON `.nav`. It was on `.nav` until fix
   time, and it was inert everywhere: `header` has no styling of its own, so
   its height equals the nav's height exactly, a sticky box cannot travel
   past the bottom of its own containing block, and the bar scrolled straight
   off screen on all seven pages. `.nav--stuck` was therefore dead code that
   could never paint. The banner landmark stays a real box (`display:contents`
   would remove it), so the offset parent for the mobile panel is unchanged —
   `.nav-inner` is `position:relative` and is still the panel's containing
   block. If you move the stickiness back down a level, re-read this.
   ========================================================================= */
header {
  position: sticky;
  top: 0;
  z-index: 60;
}
.nav {
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
}
.nav--stuck {
  background: rgba(245,242,234,0.92);
  backdrop-filter: blur(10px) saturate(1.15);
  -webkit-backdrop-filter: blur(10px) saturate(1.15);
  border-bottom-color: rgba(23,21,18,0.24);
}

.nav-inner {
  position: relative;
  max-width: 1200px;
  margin: 0 auto;
  padding: 18px 28px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

/* the logo is a link home on every page, so it is a tap target: the mark is
   28px tall and the box is padded to 44 rather than the mark being scaled up */
.nav-logo { display: inline-flex; align-items: center; min-height: 44px; text-decoration: none; }
.nav-logo img { height: 28px; width: auto; }

.nav-menu { display: flex; align-items: center; gap: 26px; }

/* The nav CTA never wraps while the desktop row is on screen. `.nav-link`
   already carries nowrap; the button did not, so between 761px (the hamburger
   has not appeared yet) and ~820px "TAKE THE FREE DIAGNOSTIC" broke over two
   lines and grew the sticky bar by ~20px on every page. Content buttons keep
   wrapping — this is scoped to the nav, and released again at <=760 where the
   CTA sits in the full-width dropdown panel and a narrow phone needs it. */
.nav-menu .btn { white-space: nowrap; }

.nav-link {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-72);
  text-decoration: none;
  white-space: nowrap;
}
.nav-link:hover { color: var(--ink); }

/* "Book a Call" sits in a row of mono uppercase labels, so it is a mono
   uppercase label. It previously overrode font-family to --sans, dropped the
   uppercase and the letter-spacing, and ran at 13px, on the reasoning that a
   booking is an invitation rather than a destination. On the page that reads
   as a mistake, not as an invitation: it is the only mixed-case item in the
   bar and the eye lands on it as unstyled text. Corrected 2026-08-25 after the
   operator flagged it.
   The distinction it was reaching for is already carried, twice: this is the
   only nav item that leaves the site, and the filled button beside it is the
   primary action. It does not also need a second typeface. */
.nav-link--plain {
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
}
.nav-link--plain:hover,
.nav-link--plain:focus-visible { border-bottom-color: var(--gold-ink); }

/* current page: a 2px gold-ink underline, offset so it does not crowd the caps */
.nav-link.is-active, .nav-link[aria-current="page"] {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--gold-ink);
  text-decoration-thickness: 2px;
  text-underline-offset: 6px;
}
.nav-link--plain.is-active, .nav-link--plain[aria-current="page"] {
  text-decoration: none;
  border-bottom: 2px solid var(--gold-ink);
}

/* hamburger — 44px box from the first pixel, not only at the breakpoint */
.nav-toggle {
  display: none;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  flex: none;
  margin-right: -10px;
}
.nav-toggle-bars { display: block; width: 22px; }
.nav-toggle-bars span {
  display: block;
  height: 2px;
  background: var(--ink);
  transition: background 0.2s var(--ease);
}
.nav-toggle-bars span + span { margin-top: 5px; }


/* =========================================================================
   8. BUTTONS
   -------------------------------------------------------------------------
   Mono caps, squared, no shadow. Three roles only: primary (the diagnostic),
   ghost (a secondary action), and the text CTA (a link that is really a
   button in intent). Vertical padding never drops below 13px, which is what
   holds the 44px tap target at mobile widths without a media query.
   ========================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1.2;
  padding: 14px 28px;
  border: 1px solid transparent;
  text-decoration: none;
  text-align: center;
  transition: background 0.15s var(--ease), color 0.15s var(--ease),
              border-color 0.15s var(--ease), transform 0.1s var(--ease);
}
/* TACTILE PRESS. A button that does nothing between mousedown and navigation
   reads as a link with a border; two percent of scale reads as a button. The
   duration is 0.1s rather than the 0.15s above deliberately: a press has to
   feel instant, and anything a person is touching should resolve faster than
   anything they are only looking at.

   Not applied to :focus-visible, which stays a ring rather than a squash, and
   cleared under reduced motion with everything else. */
.btn:active { transform: scale(0.98); }
.btn--lg { font-size: 12px; padding: 17px 36px; }
.btn--sm { font-size: 10px; padding: 11px 18px; }

.btn--primary { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.btn--primary:hover { background: #262320; color: var(--paper); border-color: #262320; }
/* on ink the primary inverts. `.band a` is (0,1,1) and `.btn--primary` is
   (0,1,0), so without these two selectors the label takes the link colour. */
.band .btn--primary, .hero .btn--primary {
  background: var(--paper);
  color: var(--ink);
  border-color: var(--paper);
}
.band .btn--primary:hover, .hero .btn--primary:hover {
  background: #fffdf7;
  color: var(--ink);
  border-color: #fffdf7;
}

/* the ghost button's border IS the button, so it takes the 3:1 control token
   rather than the decorative hairline (WCAG 1.4.11) */
.btn--ghost { background: transparent; color: var(--ink); border-color: var(--control); }
.btn--ghost:hover { color: var(--ink); border-color: var(--gold-ink); background: transparent; }
.band .btn--ghost, .hero .btn--ghost { color: var(--paper); border-color: var(--control-ink); }
.band .btn--ghost:hover, .hero .btn--ghost:hover { color: var(--paper); border-color: var(--gold); }

/* the quiet second CTA beside a primary: a link with a hairline under it */
.btn-text {
  display: inline-block;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px solid var(--hairline);
  padding: 13px 0 3px;
}
.btn-text:hover { color: var(--text-strong); border-bottom-color: var(--accent); }
.band .btn-text { color: var(--paper-88); border-bottom-color: var(--line-paper); }
.band .btn-text:hover { color: var(--paper); border-bottom-color: var(--gold); }

.actions { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.center .actions { justify-content: center; }


/* =========================================================================
   9. HAIRLINE GRIDS
   -------------------------------------------------------------------------
   gap:1px over a hairline-coloured container background, with opaque cells.
   The dividers are the gaps, so there is no border-collapse arithmetic, no
   doubled edges, and no last-child exception to write.

   grid-template-columns is NEVER set inline in the markup. An inline value
   cannot be overridden by a media query, which is exactly how a four-up grid
   ends up horizontally scrolling a phone.
   ========================================================================= */
.grid-hairline {
  display: grid;
  gap: 1px;
  background: var(--hairline);
  border: 1px solid var(--hairline);
  list-style: none;
}
.grid-hairline > * { background: var(--fill); padding: 36px 30px; min-width: 0; }

.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* one inverted cell inside a paper grid (Stage 3 on the home page). It resets
   the surface tokens the same way `.band` does, so everything inside it —
   kicker, heading, body, hairlines — recolours without a per-element rule. */
.cell--ink {
  --fill: var(--ink);
  --text-strong: var(--paper);
  --text: var(--paper-88);
  --text-dim: var(--paper-60);
  --hairline: var(--line-paper);
  --control-line: var(--control-ink);
  --accent: var(--gold);
  --focus: var(--paper);
  background: var(--ink);
  color: var(--paper-88);
}
.cell--ink h2, .cell--ink h3, .cell--ink h4, .cell--ink .h-sub { color: var(--paper); }
.cell--ink p { color: var(--paper-88); }
.cell--ink :focus-visible { outline-color: var(--paper); }

.band .grid-hairline { background: var(--line-paper); border-color: var(--line-paper); }
.band .grid-hairline > * { background: var(--ink); }

/* `.grid-hairline--zebra` alternates the cell fill so a long run of cells
   reads as a rhythm rather than as one grey field. It is scoped to the phone
   in section 13, and deliberately: at two or more columns the alternation
   lands as vertical stripes (odd cells are always the left column), which is
   noise. At one column it lands as rows, which is the thing diagnostic.html's
   five-area run actually needs — 2080px of five identical paper cells is the
   "flat, uniformly grey page" principle 2 calls a failure state. */


/* =========================================================================
   10. COMPONENTS
   ========================================================================= */

/* ---- the value-colour contract ------------------------------------------
   THE BOUNDARY IS 60. It is the Stage 2 (Systemized) threshold, it is where
   the funnel's classColor() switches, and from here on it is written down in
   exactly one place instead of three.

   Below 60 a value is still a constraint, so it takes the accent. At 60 and
   above it has cleared the line, so it takes the strong text colour and the
   accent goes back to meaning "this is the thing to fix". Gold that means
   "low" on one card and "we tagged this one" on the next is a colour language
   that contradicts itself, which is exactly what shipped before: diagnostic
   painted four sub-60 numbers in ink and one in gold, and the funnel's focus
   cards hard-coded gold onto a score of 85.

   How it works. `.is-low` / `.is-high` set ONE custom property, `--val`, and
   every value-bearing component (the gauge arc, the meter fill and number,
   the chips, the funnel's dive and focus numbers) paints with `var(--val)`.
   Nothing else changes, so a value class can be dropped on any element
   without side effects, and because --accent and --text-strong are surface
   tokens the same class does the right thing on paper and on ink.

   js/site.js derives the class from the number at runtime (applyValue), so a
   hand-authored class can only ever be the no-JS fallback, never the source
   of a disagreement between the colour and the label. Author it anyway: it is
   what a no-JS visitor sees. Override the boundary per element with
   `data-boundary` when a scale genuinely has a different midpoint. */
.is-low  { --val: var(--accent); }
.is-high { --val: var(--text-strong); }
/* the funnel's existing class names, kept as aliases so its renderers can
   move over one at a time. Prefer .is-low / .is-high in new markup. */
.is-gold  { --val: var(--accent); }
.is-paper { --val: var(--text-strong); }

/* the utility for a bare number that should carry its own value colour */
.val-num { color: var(--val, var(--text-strong)); font-variant-numeric: tabular-nums; }

/* ---- card ---------------------------------------------------------------
   A standalone hairline box, for anywhere a grid cell is not the shape. */
.card {
  background: var(--fill);
  border: 1px solid var(--hairline);
  padding: 32px 28px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.card > .kicker + h3, .card > .kicker + .h-sub { margin-top: -2px; }
.card p { font-size: 16px; }
.band .card { background: var(--ink); border-color: var(--line-paper); }

/* a card whose left edge carries the single gold accent */
.card--accent { border-left: 2px solid var(--accent); }

/* ---- stat tile ----------------------------------------------------------
   The big number is a <p>: it is a value, not a section title. The tile's
   real heading is the mono line beneath it, marked up as an h3 carrying
   .h-sub .stat-label, so the document outline stays sequential while the
   composition still leads with the number. */
.stat { display: flex; flex-direction: column; }
.stat-num {
  font-family: var(--sans);
  font-weight: 700;
  /* 50px, not 52. about's ink strip runs "$1M to $5M" beside "16 wks" and
     "30 days"; at 52px that range measured 270px against a 267px content box
     in a 3-up cell, wrapped to two lines, and pushed its label 62px below the
     other two, so the row's three labels stopped sharing a baseline on the
     highest-contrast surface on the page. It missed by three pixels. A 2em
     min-height would also have held the baseline, at the cost of 52px of dead
     air under every one-line value on every stat strip on the site; two
     points off the ceiling costs nothing anybody can see and removes the wrap
     itself. Re-measure this if a longer value is ever added to a 3-up strip. */
  font-size: clamp(38px, 4.4vw, 50px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
  margin-bottom: 6px;
}
.stat-label {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--accent);
  margin-bottom: 18px;
}
.stat-note { display: block; font-size: 14px; line-height: 1.6; color: var(--text); }
.band .stat-num { color: var(--paper); }
.band .stat-label { color: var(--gold); }
.band .stat-note { color: var(--paper-88); }

/* ---- stage card ---------------------------------------------------------- */
.stage { display: flex; flex-direction: column; }
.stage-n {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 16px;
}
.stage-name {
  display: block;
  font-family: var(--sans);
  font-weight: 700;
  font-size: 22px;
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--text-strong);
  margin-bottom: 12px;
}
.stage-note { display: block; font-size: 14px; line-height: 1.6; color: var(--text); }
.band .stage-n, .cell--ink .stage-n { color: var(--gold); }
.band .stage-name, .cell--ink .stage-name { color: var(--paper); }
.band .stage-note, .cell--ink .stage-note { color: var(--paper-88); }

/* ---- testimonial card ---------------------------------------------------
   The quote is serif: it is somebody else speaking, and the shift in voice
   does more work than any quotation mark ornament would. */
.tcard { display: flex; flex-direction: column; gap: 22px; break-inside: avoid; }
.tcard blockquote { margin: 0; }
.tquote {
  font-family: var(--serif);
  font-weight: 400;
  font-size: 17px;
  line-height: 1.62;
  color: var(--text-strong);
  /* some captured quotes carry their own line breaks; honour them */
  white-space: pre-line;
}
.tperson { display: flex; align-items: center; gap: 14px; margin-top: auto; }
.tavatar {
  width: 48px;
  height: 48px;
  flex: none;
  object-fit: cover;
  background: var(--tile);
  border: 1px solid var(--hairline);
}
.tname { display: block; font-size: 15px; font-weight: 600; color: var(--text-strong); line-height: 1.3; }
/* a job title is content, not chrome, so it takes the body ramp rather than
   the meta one even though it is set in the meta voice */
.trole {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text);
  line-height: 1.4;
  margin-top: 3px;
}
.band .tquote, .band .tname { color: var(--paper); }
.band .trole { color: var(--paper-88); }
.band .tavatar { background: rgba(245,242,234,0.08); border-color: var(--line-paper); }

/* ---- D.E.B.T. list ------------------------------------------------------- */
.debt-list { list-style: none; }
.debt-item {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr);
  gap: 20px;
  align-items: start;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--hairline);
}
.debt-item + .debt-item { padding-top: 20px; }
.debt-letter {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 26px;
  line-height: 1;
  color: var(--accent);
}
/* display:block on the parts that always own a line, so the component works
   whether a page agent reaches for <p>, <span> or <div>. Without it a pair of
   spans renders as "DIAGNOSEMap the bottleneck." and looks like a typo. */
.debt-name {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 4px;
}
.debt-note { display: block; font-size: 15px; line-height: 1.55; color: var(--text); }
.debt-letter { display: block; }
.band .debt-letter { color: var(--gold); }
.band .debt-name { color: var(--paper-60); }
.band .debt-note { color: var(--paper-88); }

/* `.debt-list--cycle` opts the list into the phase loop it shares with the
   diagram (see the D.E.B.T. loop block below and section 12). Its only
   non-animated effect is the resting letter colour: four gold letters
   permanently lit is four accents in one composition, and it leaves the loop
   nothing to light UP to. At rest the letters take the body ramp (6.6:1 on
   paper-alt) and the active phase is the one that goes gold.

   Gated on `.js` for the same reason every other motion rule is. Without the
   loop running there is nothing to distinguish, and the original all-gold set
   is the better still image, so a no-JS visitor keeps it. Reduced motion gets
   it back too, via a matching-specificity rule in section 14. */
.js .debt-list--cycle .debt-letter { color: var(--text); }
.js .band .debt-list--cycle .debt-letter { color: var(--paper-88); }

/* ---- numbered steps (how it works) -------------------------------------- */
.steps { list-style: none; display: grid; gap: 28px; }
.steps > li { display: grid; grid-template-columns: 46px minmax(0, 1fr); gap: 20px; align-items: start; }
.step-n {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  color: var(--accent);
  width: 46px;
  height: 46px;
  border: 1px solid var(--hairline);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.step-body h3, .step-body .h-sub { margin-bottom: 6px; }
.step-body p { font-size: 15px; line-height: 1.65; }
/* the marker box is the one gold accent in a steps list, so on ink it takes
   the soft gold hairline rather than the neutral one */
.band .step-n { color: var(--gold); border-color: var(--gold-soft); }

/* ---- spec table (the practice / what it costs) --------------------------- */
.spec { border-top: 1px solid var(--hairline); }
.spec-row {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: 48px;
  padding: 28px 0;
  border-bottom: 1px solid var(--hairline);
}
.spec-k {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  padding-top: 3px;
}
.spec-v { display: block; font-size: 15px; line-height: 1.7; color: var(--text); }
.band .spec-v { color: var(--paper-88); }

/* ---- FAQ ----------------------------------------------------------------
   `.faq-q` / `.faq-a` are the pair. The `.faq` list wrapper and its `> li`
   rule were deleted at fix time with zero uses on any page: methodology
   builds its four Q&As as a 2x2 `.grid-hairline`, which is the system's own
   container, so a second list chrome for the same job was a pattern the site
   invented and never deployed (principle 6). The two text classes stay,
   because the grid cells use them. */
.faq-q { display: block; font-size: 19px; font-weight: 600; color: var(--text-strong); margin-bottom: 10px; letter-spacing: -0.01em; }
.faq-a { display: block; font-size: 16px; line-height: 1.7; color: var(--text); max-width: 68ch; }
.band .faq-a { color: var(--paper-88); }

/* ---- photo / diagram tile ------------------------------------------------ */
.tile-fill { background: var(--tile); }
.portrait {
  width: 100%;
  height: auto;
  background: var(--tile);
  border: 1px solid var(--hairline);
}
.figure-cap {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-top: 16px;
}
.band .figure-cap { color: var(--paper-60); }

/* ---- donut gauge --------------------------------------------------------
   ONE gauge, data driven, used by the home page, the diagnostic samples and
   the funnel's live report. Before this pass there were two: index drew r=52
   and diagnostic drew r=60, both with a hand-computed stroke-dasharray and a
   hand-picked colour class, so the "same" component disagreed with itself on
   two pages and the colour encoded the author's opinion rather than the value.

   THE CONTRACT (page agents build to this exactly):

     <figure class="gauge">
       <svg class="gauge-svg" data-score="34" viewBox="0 0 136 136"
            role="img" aria-labelledby="g1">
         <title id="g1">A Founder Bottleneck Score of 34 out of 100 ...</title>
         <circle class="gauge-track" cx="68" cy="68" r="56" />
         <circle class="gauge-arc is-low" cx="68" cy="68" r="56"
                 stroke-dasharray="119.63 232.23" transform="rotate(-90 68 68)" />
         <text class="gauge-num" x="68" y="72" text-anchor="middle">34</text>
         <text class="gauge-den" x="68" y="88" text-anchor="middle">/100</text>
       </svg>
       <figcaption class="gauge-cap">Stage 0 &middot; Trapped</figcaption>
     </figure>

   THE BASELINES ARE 72 AND 88, measured optically against the 28px default
   `.gauge-num`, and index.html and diagnostic.html both ship them. (This
   comment said 73 / 89 until fix time and nothing on the site used those.) A
   gauge that OVERRIDES the number's font-size has to re-derive its own pair:
   `.fn-score-gauge` runs `.gauge-num` at 44px and therefore sits at 76 / 96.
   Change the size, re-measure the baselines; they are not portable.

   ONE RADIUS SITE-WIDE: r=56 in a 136 viewBox. Circumference 351.86. The
   static dasharray is `C*score/100` then `C*(1-score/100)`, and it ships in
   the markup so a no-JS or reduced-motion visitor sees a finished, correct
   ring rather than an empty one — the same discipline the count-up follows by
   shipping the finished string.

   The value class on the ARC (not the svg) is the no-JS colour. site.js
   re-derives it from data-score every frame of the fill, so on the JS path
   the colour physically travels with the number: an 82 starts gold, crosses
   the 60 boundary partway round the ring, and lands in ink. A low score and a
   high score are therefore different states you can see from across a room,
   and the arc can never contradict the stage label underneath it.

   Geometry stays in the markup as presentation attributes because it is data.
   Colour lives here, so a gauge dropped on a band recolours with the system.
   ------------------------------------------------------------------------- */
.gauge { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.gauge-svg { width: 136px; height: 136px; overflow: visible; }
.gauge--sm .gauge-svg { width: 104px; height: 104px; }
.gauge--lg .gauge-svg { width: 176px; height: 176px; }
.gauge-track { fill: none; stroke: rgba(23,21,18,0.10); stroke-width: 8; }
/* The arc paints from --val, so the value classes above are the only thing
   that decides its colour. The stroke transition is what makes the boundary
   crossing read as a colour travelling with the fill rather than as a snap;
   it is well inside the 1400ms fill, so it has resolved before the ring
   finishes. Same easing curve as everything else. */
.gauge-arc {
  fill: none;
  stroke: var(--val, var(--accent));
  stroke-width: 8;
  stroke-linecap: butt;
  transition: stroke 0.45s var(--ease);
}
/* The value contract, at (0,2,0) so the class site.js writes every frame
   always out-orders anything an author could hand-set on the arc. That is the
   invariant the component rests on: the colour is a function of the number,
   never of the author. (A deprecated `.gauge-arc--ink` compatibility class
   lived here until fix time; zero uses, deleted.) */
.gauge-arc.is-low  { --val: var(--accent); }
.gauge-arc.is-high { --val: var(--text-strong); }
.gauge-num {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 28px;
  fill: var(--ink-92);
  font-variant-numeric: tabular-nums;
}
/* opt-in: let the number carry the value colour too, for a report where the
   score IS the composition rather than a figure beside a caption */
.gauge-num--val { fill: var(--val, var(--text-strong)); transition: fill 0.45s var(--ease); }
.gauge-den {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.1em;
  fill: var(--ink-72);
}
.gauge-cap {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-align: center;
}
.band .gauge-track { stroke: rgba(245,242,234,0.14); }
.band .gauge-num { fill: var(--paper); }
.band .gauge-den { fill: var(--paper-60); }
.band .gauge-cap { color: var(--paper-60); }
.gauges { display: flex; gap: 48px; justify-content: center; flex-wrap: wrap; }

/* ---- meter (the horizontal twin of the gauge) ---------------------------
   ONE bar for "an area score out of 100", replacing two inventions: the ten
   raw `<svg height="7"><rect>` bars hand-written into diagnostic.html and the
   funnel's own `.fn-meter` div pair. Same value contract as the gauge, same
   60 boundary, same reveal-driven fill.

   THE CONTRACT:

     <div class="meter is-low" data-value="42" style="--meter-w:42%">
       <p class="meter-top">
         <span class="meter-name">Delivery</span>
         <span class="meter-num" data-count="42">42</span>
       </p>
       <span class="meter-track"><span class="meter-fill"></span></span>
     </div>

   `--meter-w` is the one inline style this component takes, on the same
   justification the funnel's bar widths already have: a bar length IS the
   datum being drawn and there is no class-based way to say "42%".

   Resting states. Without JS the fill is simply at its final width, because
   `--meter-w` is the declared value. With JS it starts at 0 and the
   `.is-filled` class (added by site.js on scroll-in, or inherited from a
   `.reveal` ancestor) transitions it to width over 1400ms, in step with the
   count-up on `.meter-num`. Reduced motion pins it at the final width. There
   is no state in which a visitor sees a half-drawn bar and reads it as data.
   ------------------------------------------------------------------------- */
.meter { display: block; color: var(--val, var(--text-strong)); }
.meter-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 9px;
}
.meter-name { font-size: 15px; line-height: 1.4; color: var(--text); }
/* the number and its denominator travel together on the right; without the
   wrapper a three-child flex row with space-between throws "/100" against the
   far edge and the number floats in the middle of the row */
.meter-val { display: inline-flex; align-items: baseline; gap: 4px; flex: none; }
/* Reads --val directly rather than currentColor. `.meter-top` is a <p>, and
   `.band p` is (0,1,1), so a currentColor number inside a band inherits the
   paragraph's body colour and the value language silently disappears on the
   one surface the funnel report actually uses. The fill is a <span> chain
   with no type-selector override in its path, so it can keep currentColor. */
.meter-num {
  font-family: var(--mono);
  font-size: 20px;
  font-weight: 600;
  line-height: 1;
  color: var(--val, var(--text-strong));
  font-variant-numeric: tabular-nums;
}
.meter-den {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  color: var(--text-dim);
}
.band .meter-den { color: var(--paper-60); }
.meter-track { display: block; height: 3px; background: var(--hairline); }
.meter-fill {
  display: block;
  height: 3px;
  background: currentColor;
  width: var(--meter-w, 0%);
}
/* the JS path: start empty, fill on reveal. Both hooks are supported — the
   component's own `.is-filled` (set by site.js) and an `.is-revealed`
   ancestor, so a meter inside an existing `.reveal` block needs no extra JS
   and a standalone one still fills. */
.js .meter-fill { width: 0; transition: width 1.4s var(--ease); }
.js .meter.is-filled .meter-fill,
.js .is-revealed .meter-fill { width: var(--meter-w, 0%); }
/* the row that is the binding constraint: the accent plus a mono tag */
.meter-tag {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-left: 8px;
  white-space: nowrap;
}
.band .meter-name { color: var(--paper-88); }
.band .meter-tag { color: var(--gold); }
/* a stack of meters read as one comparison: shared baseline, even rhythm */
.meter-list { display: grid; gap: 20px; list-style: none; }

/* ---- step track ---------------------------------------------------------
   "Phase 2 of 4" and "Stage 0 -> Stage 3" said as a mark instead of as four
   identical text columns. Squares and hairlines, no new visual language.

     <p class="track" role="img" aria-label="Phase 2 of 4">
       <span class="track-seg is-on"></span><span class="track-seg is-on"></span>
       <span class="track-seg"></span><span class="track-seg"></span>
     </p>

   role="img" + aria-label because the segments carry no text: without it a
   screen reader gets four empty spans. */
.track { display: flex; align-items: center; gap: 6px; max-width: 220px; }
.track-seg { flex: 1 1 0; min-width: 0; height: 4px; background: var(--hairline); }
.track-seg.is-on { background: var(--accent); }
.track--full { max-width: none; }
.track-cap {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 10px;
}

/* ---- area icon + icon box -----------------------------------------------
   Promoted out of the funnel's inline sheet so diagnostic.html can print the
   same five area marks the funnel uses, instead of the cryptic S / D / O /
   SM / TR mono abbreviations it invented. The five 24x24 paths live in the
   markup (and in the funnel's areaIcon()); this is the shared frame. */
.area-icon {
  width: 26px;
  height: 26px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: square;
}
.area-icon--lg { width: 28px; height: 28px; }
.iconbox {
  display: inline-flex;
  padding: 13px;
  border: 1px solid var(--hairline);
  color: var(--accent);
}
.band .iconbox { color: var(--gold); }

/* ---- dependency map -----------------------------------------------------
   The signature mark: founder square at centre, five area squares around it,
   one line each. Stroke weight encodes dependency, so the heavy lines are the
   argument. Colours here, geometry in the markup.

   The `.depmap-line--heavy/mid/light` classes are the only thing that varies
   between the hero's illustrative map and any other static instance. */
.depmap { max-width: 420px; }
.depmap-svg { width: 100%; height: auto; overflow: visible; }
/* WIDTH COMES FROM A CUSTOM PROPERTY so a generated map can pass a
   CONTINUOUS value while a hand-authored one keeps the banded classes.

   The banded scale quantises every score from 0 to 39 to the same stroke,
   which is wrong on the commonest real profile the practice targets: Sales
   at 8 against four areas at 25 drew five identical spokes under a caption
   reading "the heavier the line, the more that area still routes through the
   founder", with the numbers 8 / 25 / 25 / 25 / 25 printed beside them. The
   reader is told to read weight and there is no weight to read.

   A stroke width that IS the datum is the same exception BUILD-CONTRACT
   section 0 already grants `--meter-w`: there is no class-based way to spell
   an arbitrary length. Setting it as a custom property rather than as
   `stroke-width` means an inline value beats a class without a specificity
   fight, and a caller that has no continuous value simply does not set it. */
.depmap-line { stroke: var(--gold); stroke-width: var(--depmap-w, 7); fill: none; }
.depmap-line--heavy { --depmap-w: 10; }
.depmap-line--mid { --depmap-w: 7; }
.depmap-line--light { --depmap-w: 4; stroke: rgba(245,242,234,0.35); }
/* The five-step weight scale the funnel's live map needs. The three named
   weights above are the home page's illustrative set; a map built from real
   scores wants finer resolution, so the numeric twins exist and both spellings
   drive the same thing. Adding them here is what lets the funnel's generated
   map drop the parallel `.fn-w10 / .fn-w8 / ...` vocabulary and use the
   shared component, which is the whole point of section 6 of the principles. */
.depmap-line--w10 { --depmap-w: 10; }
.depmap-line--w8  { --depmap-w: 8; }
.depmap-line--w7  { --depmap-w: 7; }
.depmap-line--w4  { --depmap-w: 4; }
.depmap-line--w2  { --depmap-w: 2; }
/* the fill transition is for a map whose data changes in place (the funnel
   lights the area being answered); it is inert on a static instance */
.depmap-node { fill: var(--paper); stroke: var(--ink); stroke-width: 4; transition: fill 0.6s var(--ease); }
.depmap-node--dep { fill: var(--gold); }
/* "Not measured yet" needs to be a STATE, not an absence. The funnel's intro
   map drew every outer node as `rgba(245,242,234,0.15)` carrying the inherited
   `stroke: var(--ink)` — an ink stroke on an ink ground — so the five
   endpoints of the first mark a cold visitor sees effectively vanished and the
   gold spokes terminated in stubs. The boundary now takes --control-ink, which
   measures 3.79:1 on --ink and therefore clears WCAG 1.4.11 for non-text
   content, over a barely-there fill so the square still reads as unfilled. */
.depmap-node--empty { fill: rgba(245,242,234,0.06); stroke: var(--control-ink); }
.depmap-core { fill: var(--gold); stroke: var(--ink); stroke-width: 5; }
/* The travelling pulse. One `.depmap-trace` per spoke, geometry identical to
   the `.depmap-line` under it, painted in paper so it reads as a bright segment
   moving over the gold rather than as a sixth colour in a nine-colour system.

   Each trace is about 70% of the weight of the line it rides. Three widths
   were rendered and compared at the same frame: at full weight the pulse
   covers the spoke edge to edge and the gold reads as briefly broken, which
   looks like a gap in the mark rather than something moving through it; at
   half weight it thins into a glint and stops reading as a discrete pulse.
   At 70% a gold margin survives down both sides, so the spoke stays continuous
   and the pulse still reads as a solid block travelling along it.

   Brightness steps down with dependency, so the light spoke gets the dimmest
   traveller as well as the slowest.

   Declared at opacity 0 and only animated under `.js .depmap--live`. A trace is
   a decorative duplicate of a line the <title> already describes, so the group
   that holds them is aria-hidden in the markup, and if the animation is ever
   dropped these five elements simply are not there. */
/* The pulse rides the line, so its width tracks the same property, at 0.7
   of it: the travelling segment reads as a highlight moving OVER the spoke
   rather than as a second spoke. */
.depmap-trace {
  fill: none;
  stroke-dasharray: 22 340;
  opacity: 0;
  stroke-width: calc(var(--depmap-w, 7) * 0.7);
}
.depmap-trace--heavy { stroke: var(--paper); --depmap-w: 10; }
.depmap-trace--mid { stroke: var(--paper-88); --depmap-w: 7; }
.depmap-trace--light { stroke: var(--paper-60); --depmap-w: 3.6; }
.depmap--paper .depmap-trace--heavy { stroke: var(--ink); }
.depmap--paper .depmap-trace--mid { stroke: var(--ink-92); }
.depmap--paper .depmap-trace--light { stroke: var(--ink-66); }
/* the numeric twins, brightness stepping down with weight exactly as the
   three named ones do. A trace is always ~70% of the width of the line it
   rides: see the note above for the three renders that settled that number. */
.depmap-trace--w10 { stroke: var(--paper); --depmap-w: 10; }
.depmap-trace--w8  { stroke: var(--paper); --depmap-w: 8; }
.depmap-trace--w7  { stroke: var(--paper-88); --depmap-w: 7; }
.depmap-trace--w4  { stroke: var(--paper-88); --depmap-w: 4; }
.depmap-trace--w2  { stroke: var(--paper-60); --depmap-w: 2.3; }
.depmap--paper .depmap-trace--w10 { stroke: var(--ink); }
.depmap--paper .depmap-trace--w8  { stroke: var(--ink); }
.depmap--paper .depmap-trace--w7  { stroke: var(--ink-92); }
.depmap--paper .depmap-trace--w4  { stroke: var(--ink-92); }
.depmap--paper .depmap-trace--w2  { stroke: var(--ink-66); }

.depmap-label {
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.14em;
  fill: var(--paper-60);
  text-transform: uppercase;
}
.depmap-cap {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold);
  text-align: center;
  margin-top: 22px;
}
/* the same mark on a paper ground: gold is 2.22:1 there, so the twin drives */
.depmap--paper .depmap-line { stroke: var(--gold-ink); }
.depmap--paper .depmap-line--light { stroke: rgba(23,21,18,0.30); }
.depmap--paper .depmap-node { fill: var(--paper); stroke: var(--ink); }
.depmap--paper .depmap-node--dep { fill: var(--gold-ink); }
.depmap--paper .depmap-core { fill: var(--gold-ink); }
.depmap--paper .depmap-label { fill: var(--ink-66); }
.depmap--paper .depmap-cap { color: var(--gold-ink); }

/* ---- the D.E.B.T. loop diagram ------------------------------------------
   The max-width is the component's own, finally. methodology.html was
   wrapping a `<figure class="loop">` in a `<div class="depmap">` purely to
   borrow that class's 420px cap, and its own comment asked for this: a
   dependency-map class wrapping a loop diagram is the invented pattern
   principle 6 exists to catch. Remove that wrapper.

   Why the cap matters at all: every diagram in this family authors its text
   and stroke weights in USER UNITS, so an uncapped figure at the <=900
   single-column breakpoint scales a 9.5px mono label up to about 21px.

   Two sizes, because there are two viewBoxes. The 324 and 420 unit diagrams
   take 420px; the 640-unit D.E.B.T. loop on methodology takes 760px. That
   second cap is selected off the SVG child rather than off a class on the
   figure, so it cannot be lost by a page agent forgetting a modifier — but
   `.loop--wide` is there as the explicit spelling for anyone who prefers it. */
.loop { margin: 0; max-width: 420px; }
.loop--wide, .loop:has(.loop-svg--wide) { max-width: 760px; }
.loop-svg { width: 100%; height: auto; overflow: visible; }
.loop-line { stroke: var(--ink); stroke-width: 1; fill: none; }
.loop-arrow { fill: var(--ink); }
.loop-line--gold { stroke: var(--gold-ink); stroke-width: 2; }
.loop-arrow--gold { fill: var(--gold-ink); }
.loop-box { fill: none; stroke: var(--ink); stroke-width: 1.25; }
.loop-letter { font-family: var(--sans); font-weight: 700; font-size: 26px; fill: var(--ink); }
/* the four phase names are the content of the diagram, so they take the body
   ramp; only the caption running through the middle stays in the accent */
.loop-name {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  fill: var(--ink-72);
}
.loop-cap {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.18em;
  fill: var(--gold-ink);
}
.band .loop-line { stroke: var(--paper-60); }
.band .loop-arrow { fill: var(--paper-60); }
.band .loop-line--gold { stroke: var(--gold); }
.band .loop-arrow--gold { fill: var(--gold); }
.band .loop-box { stroke: var(--paper-60); }
.band .loop-letter { fill: var(--paper); }
.band .loop-name { fill: var(--paper-60); }
.band .loop-cap { fill: var(--gold); }

/* ---- the D.E.B.T. loop, animated ----------------------------------------
   `.loop--cycle` on the figure and `.debt-list--cycle` on the list opt one
   pairing into the four-phase loop. Both are needed: the list and the diagram
   are two views of the same cycle and they light together. methodology.html
   carries three more `.loop-svg` instances and takes neither class, so its
   diagrams stay exactly as they are.

   The four keyframe sets in section 12 paint through these six custom
   properties rather than reaching for tokens directly, which is what lets one
   set of keyframes serve both surfaces: `.band` reassigns the six and the
   animation recolours itself, the same trick `.band` plays on the surface
   layer. A keyframe that hard-coded var(--ink) would render an invisible
   "rest" state the first time somebody dropped this figure on an ink ground.
   ------------------------------------------------------------------------- */
.loop--cycle, .debt-list--cycle {
  --loop-rest: var(--ink);            /* box stroke + letter at rest */
  --loop-rest-name: var(--ink-72);    /* the phase name at rest */
  --loop-rest-dim: var(--ink-66);     /* the list's mono name at rest */
  --loop-active: var(--gold-ink);     /* attention has arrived */
  --loop-active-name: var(--ink-92);
  --loop-lift: rgba(194,160,90,0.11); /* the restrained fill under an active box */
  --loop-lift-0: rgba(194,160,90,0);  /* the same colour at zero, so it interpolates */
}
.band .loop--cycle, .band .debt-list--cycle {
  --loop-rest: var(--paper-60);
  --loop-rest-name: var(--paper-60);
  --loop-rest-dim: var(--paper-60);
  --loop-active: var(--gold);
  --loop-active-name: var(--paper);
  --loop-lift: rgba(194,160,90,0.16);
}
/* `fill: none` cannot interpolate to a colour, so an animated box rests on a
   fully transparent gold instead. Renders identically. */
.loop--cycle .loop-box { fill: var(--loop-lift-0); }

/* The connector travellers: one per arrow, geometry duplicated from the
   hairline beneath so the hairline can stay drawn while the pulse crosses it.
   The gold return trace is the loop's payoff beat, so it is the one that gets
   a longer dash and a heavier stroke. */
.loop-trace {
  fill: none;
  stroke: var(--loop-active);
  stroke-width: 2.5;
  stroke-dasharray: 18 340;
  opacity: 0;
}
.loop-trace--td { stroke-width: 3.5; stroke-dasharray: 24 340; }
/* the arrowheads bloom as the traveller lands on them */
.loop--cycle .loop-arrow, .loop--cycle .loop-arrow--gold {
  transform-box: fill-box;
  transform-origin: center;
}

/* `.loop-svg--wide` is the 640-unit D.E.B.T. loop on methodology.html. Every
   other diagram in this family is 324-420 units, so at a phone width they land
   near 1:1 while the wide cut renders at about 0.55 and its 9.5px labels come
   out at 5px. SVG font-size is in user units and a CSS rule out-specifies an
   SVG presentation attribute, so the size is stepped back up here. The two
   steps below keep the rendered label between 8 and 11px from 320px to 640px.
   Ceiling on the caption: at 17px the 20-character string is 252 units wide
   and the gap between the two top boxes is 248, so it stays a step behind. */
@media (max-width: 600px) {
  .loop-svg--wide .loop-name, .loop-svg--wide .loop-cap { font-size: 13px; }
}
@media (max-width: 440px) {
  .loop-svg--wide .loop-name { font-size: 17px; }
  .loop-svg--wide .loop-cap { font-size: 16px; }
}

/* ---- hero ---------------------------------------------------------------
   Ink ground. `.hero` reassigns the surface tokens itself rather than relying
   on a `.band` class beside it, so a page agent cannot get half a hero by
   forgetting one of the two. */
.hero {
  --ground: var(--ink);
  --fill: var(--ink);
  --text-strong: var(--paper);
  --text: var(--paper-88);
  --text-dim: var(--paper-60);
  --hairline: var(--line-paper);
  --control-line: var(--control-ink);
  --accent: var(--gold);
  --focus: var(--paper);
  background: var(--ink);
  color: var(--paper-88);
  padding: 96px 0 104px;
}
/* ---- The headline measure -------------------------------------------------
   The h1 is two sentences and it is set as two block-level spans, one each, so
   the engine balances the two rags independently. Before that it was one
   paragraph with a <br /> in the middle: the browser filled the first line
   greedily to the 680px copy column and dropped "grow." onto a line of its
   own at 1440. A single orphaned word under a 64px headline is the most
   expensive-looking mistake a hero can make.

   `text-wrap: balance` is what removes the orphan rather than a hand-placed
   break, because the break point has to move as the column narrows, and
   balance is what makes the fix hold at widths nobody screenshotted: it
   minimises the longest line, so the split it picks is never the one that
   strands a word. No explicit responsive break was needed at any width.
   Verified by screenshot at 1440, 1200, 1024, 900, 768, 430 and 390:

     1440 / 1200 / 1024   680px copy column, two lines per sentence, 2+3 / 3+3
     900 / 768            stacked, each sentence on one line
     430 / 390            two lines per sentence, 2+3 / 3+3

   The 24ch cap is a backstop for a copy column wider than the grid can
   actually produce (it works out near 770px at the 64px ceiling, and the
   widest the column ever gets is 680px), so on desktop the grid governs and
   the cap never bites. At <=900 it is released entirely, which is what lets
   each sentence take one full line in the stacked layout.
   ------------------------------------------------------------------------- */
.hero h1 { font-weight: 700; color: var(--paper); max-width: 24ch; }
.hero h1 .hero-line { display: block; text-wrap: balance; }

/* The punch line carries the one accent in the composition. --gold measures
   7.35:1 on --ink, so it clears AA at any size; it is colour only, no weight
   change, no decoration, and the span is inside the h1 so assistive tech still
   reads one heading of plain text. Paired with the muted hero kicker below:
   two golds in one composition is one gold too many, and the headline wins. */
.hero h1 .hero-line--punch { color: var(--gold); }

/* Hero scope only — .kicker stays --gold on every other .band. Above a gold
   headline a gold kicker reads as a second accent competing at 11px; in
   paper-60 it reads as what it is, a system label introducing the headline. */
/* The `color` on this rule is load-bearing and must not be deleted as
   "redundant with --accent". `.hero p` below is (0,1,1) and a bare `.kicker`
   is (0,1,0), so a `<p class="kicker">` inside a hero takes the body colour
   unless something at (0,2,0) says otherwise. Removing it during this pass
   turned 404.html's gold "404" — that page's only accent — into pale cream,
   which is exactly the specificity trap BRAND-SYSTEM section 6 warns about.

   The mute is therefore opt-in per element rather than scoped to `.hero`,
   because `.hero` is also 404.html's ground. Two compositions were rendered
   and compared before choosing which:

     A (kept)      quiet kicker, setup line stays full --paper
     B (rejected)  kicker stays gold, setup line dimmed to --paper-60

   B put the setup sentence at a lower contrast than the body paragraph
   underneath it, which inverts the hierarchy: a 64px headline reading fainter
   than its own 19px sub looks disabled rather than subordinate. It also left
   two golds still competing, because dimming the setup line does nothing about
   the kicker. A leaves exactly one gold in the composition, and an 11px mono
   label on the meta ramp reads as label-then-headline rather than as a second
   accent. */
.hero .kicker { color: var(--gold); margin-bottom: 26px; }
/* the quiet twin, at the same (0,2,0) and after it, so it actually wins */
.hero .kicker--quiet { color: var(--paper-60); }

/* `pretty` is the sub's half of the headline fix: it reserves the last line so
   the paragraph cannot end on a lone word. At 1440 this paragraph used to end
   with "it." alone on a third line, directly under the orphaned "grow.".
   Global, because a paragraph that cannot end on a lone word is an improvement
   on every page that uses this class. */
.hero-sub {
  font-size: clamp(17px, 2vw, 19px);
  line-height: 1.6;
  color: var(--paper-88);
  max-width: 52ch;
  text-wrap: pretty;
  margin: 28px 0 44px;
}
/* The measure change is NOT global. `.hero-sub` is also methodology.html's
   deck and 404.html's one-liner, and widening a shared measure to fix this
   page's rag would reflow theirs. At 52ch `pretty` does its job here (nothing
   is stranded) but the paragraph lands as three lines with a two-word tail;
   two more characters of room turns it into two full lines at every width from
   768 up. `.hero-copy` is the home hero's copy column and appears nowhere
   else, so it is the honest scope for a fix that belongs to this composition. */
.hero-copy .hero-sub { max-width: 54ch; }
.hero p { color: var(--paper-88); }
/* the headline is the asset, so the copy column takes the larger share and
   the mark sits beside it rather than competing for width */
.hero-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.75fr); gap: 56px; align-items: center; }
.hero-visual { justify-self: center; width: 100%; max-width: 380px; }

/* A hero that has to hold the screen on its own. 404.html is 1399px tall at
   390 and 896 of that is the footer, so the message reads as a caption above
   a footer rather than as the page. Vertically centring it inside 60vh gives
   it the screen. Applied automatically to the only page that is entirely one
   ink hero, and available as an explicit class for the next one. */
.hero--hold, body.page-ink main {
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Compact interior hero. Paper ground by default, but about.html ships
   `class="band page-hero"` to get this heading weight on ink, so the rule
   reads --hairline (which .band reassigns) rather than --line directly. */
.page-hero { padding: 72px 0 56px; border-bottom: 1px solid var(--hairline); }
/* Was 20ch. That cap, not the engine, is what forced "The Founder Bottleneck
   Score" to break and stranded "Score(TM)" alone on line two at 1440, 1024 AND
   768 — 28 characters against a 20ch ceiling can only break. With
   `text-wrap: balance` now on every h1 the cap has nothing left to do except
   cause that, so it is released to 30ch, which still stops a 1200px h1 from
   running the full container measure. */
/* Weight 600, explicitly. about.html ships `class="hero page-hero"` (it needs
   `.hero` for the gold `.hero-line--punch` device), which pulls in
   `.hero h1 { font-weight: 700 }`. Contract section 2 allows 700 on the home
   hero, singular, and two 700-weight h1s on one site is drift. The interior
   heroes sit at 600 like every other h1. */
.page-hero h1 { max-width: 30ch; font-weight: 600; }
.page-hero .kicker { margin-bottom: 20px; }
.page-hero .lede { margin-top: 22px; }

/* ---- shared form primitives ---------------------------------------------
   The score gate keeps its own styles inline (it is rendered by JS inside
   score/index.html). These cover any static form the marketing pages grow,
   and give the funnel a shared vocabulary to converge on later. */
.field { display: block; margin-bottom: 20px; }
.field > label, .field-label {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 8px;
}
.field input, .field select, .field textarea {
  width: 100%;
  font-family: var(--sans);
  font-size: 16px;
  padding: 13px 15px;
  background: var(--paper);
  color: var(--ink);
  /* a control boundary is non-text content under WCAG 1.4.11 and needs 3:1,
     which the decorative hairline tokens do not reach. See --control. */
  border: 1px solid var(--control-line);
}
.field input::placeholder, .field textarea::placeholder { color: var(--ink-66); }
.field-req { color: var(--gold-ink); font-family: var(--mono); font-size: 10px; }
.band .field input, .band .field select, .band .field textarea {
  background: rgba(245,242,234,0.07);
  color: var(--paper);
  border-color: var(--control-ink);
}
.band .field-req { color: var(--gold); }
.form-error {
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.6;
  color: var(--gold-ink);
  border-left: 2px solid var(--gold-ink);
  padding: 12px 16px;
  margin-bottom: 18px;
}
.band .form-error { color: var(--gold); border-left-color: var(--gold); }


/* =========================================================================
   11. FOOTER
   -------------------------------------------------------------------------
   One canonical footer on every page, ink ground. It reassigns the same
   surface tokens `.band` does, so the components inside it need no footer
   specific colour rules.
   ========================================================================= */
.footer {
  --ground: var(--ink);
  --fill: var(--ink);
  --text-strong: var(--paper);
  --text: var(--paper-88);
  --text-dim: var(--paper-60);
  --hairline: var(--line-paper);
  --control-line: var(--control-ink);
  --accent: var(--gold);
  --focus: var(--paper);
  background: var(--ink);
  color: var(--paper-88);
  padding: 64px 0 36px;
  border-top: 1px solid var(--line-paper);
}
.footer-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) repeat(3, minmax(0, 1fr));
  gap: 48px;
  align-items: start;
}
.footer-brand img { height: 22px; width: auto; margin-bottom: 16px; }
.footer-brand p {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-60);
  max-width: 24ch;
  line-height: 1.6;
}
.footer-col h2, .footer-col .footer-h {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 14px;
}
.footer-col ul { list-style: none; }
.footer-col a {
  display: inline-block;
  font-family: var(--sans);
  font-size: 14px;
  color: var(--paper-88);
  text-decoration: none;
  padding: 5px 0;
}
.footer-col a:hover { color: var(--paper); text-decoration: underline; text-underline-offset: 3px; }
.footer-bottom {
  margin-top: 48px;
  padding-top: 22px;
  border-top: 1px solid var(--line-paper);
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.footer-bottom p, .footer-bottom span {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.14em;
  color: var(--paper-60);
}


/* =========================================================================
   12. MOTION
   -------------------------------------------------------------------------
   One easing curve, cubic-bezier(0.22,0.61,0.36,1), held in --ease. Do not
   introduce a second.

   Everything here is gated on `html.js`, which the inline head script adds.
   Content is visible by default and motion is the enhancement, so a failed
   or blocked site.js leaves a readable page rather than a blank one. That
   inline script also carries a 4s watchdog that force-reveals anything still
   hidden, and an error listener that removes the class outright.

   No will-change on .reveal: it is a standing request for a compositor layer,
   one per element, held for the life of the page. Browsers already promote an
   element for the duration of an opacity or transform transition.
   ========================================================================= */
.js .reveal {
  opacity: 0;
  transform: translateY(10px);
  /* THE BLUR IS THE POINT, and it is why this reads as a camera pulling focus
     rather than a block sliding up. Fade plus travel alone is the move every
     template ships; the 2px defocus is what almost nobody adds, and it is
     doing more work here than the distance is.

     Travel came DOWN from 22px when the blur went in. The two are substitutes:
     with a defocus carrying the entrance, a long slide reads as fussy, and a
     short one reads as deliberate. Ten pixels and two pixels of blur.

     SAFE AGAINST THE `filter` TRAP. A filter makes an element a containing
     block for fixed and absolute descendants, which silently breaks sticky
     positioning inside it. The only sticky element on this site is `header`,
     which is never inside a `.reveal`. If that ever changes, this rule is the
     thing that broke it. */
  filter: blur(2px);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease),
              filter 0.7s var(--ease);
}
.js .reveal.is-revealed { opacity: 1; transform: none; filter: none; }

/* count-up numbers hold their column width so the layout does not jitter */
.countup, [data-count] { font-variant-numeric: tabular-nums; }

/* ---- hero dependency map: line draw -------------------------------------
   dasharray is a flat 420 rather than a per-line measured length. Every line
   in the mark is shorter than that, so one dash covers any of them and the
   offset animation reads identically on all five. Measuring each would mean
   an inline custom property per line, which the markup rules forbid.

   The stagger is nth-of-type here rather than inline transition-delay: these
   are SVG children of one group, not a stack of reveal siblings, and the
   order is fixed by the geometry.

   Fill mode is `both`, and every hidden start state lives in a keyframe's
   `from` rather than in the rule's own declarations. That ordering matters:
   if an animation is ever dropped by the engine, the element falls back to
   its declared state, which here is a fully drawn, fully opaque mark. Put the
   start state in the declaration instead and a dropped animation leaves the
   hero visual permanently blank, which is precisely the failure mode this
   whole js-gating discipline exists to prevent.
   ------------------------------------------------------------------------- */
.js .depmap--draw .depmap-line {
  stroke-dasharray: 420;
  animation: gt-draw 1.05s var(--ease) both;
}
.js .depmap--draw .depmap-line:nth-of-type(1) { animation-delay: 0.15s; }
.js .depmap--draw .depmap-line:nth-of-type(2) { animation-delay: 0.28s; }
.js .depmap--draw .depmap-line:nth-of-type(3) { animation-delay: 0.41s; }
.js .depmap--draw .depmap-line:nth-of-type(4) { animation-delay: 0.54s; }
.js .depmap--draw .depmap-line:nth-of-type(5) { animation-delay: 0.67s; }

.js .depmap--draw .depmap-node { animation: gt-fade 0.5s var(--ease) both; }
.js .depmap--draw .depmap-node:nth-of-type(1) { animation-delay: 0.85s; }
.js .depmap--draw .depmap-node:nth-of-type(2) { animation-delay: 0.94s; }
.js .depmap--draw .depmap-node:nth-of-type(3) { animation-delay: 1.03s; }
.js .depmap--draw .depmap-node:nth-of-type(4) { animation-delay: 1.12s; }
.js .depmap--draw .depmap-node:nth-of-type(5) { animation-delay: 1.21s; }

/* The arrival pulse: once, on the draw-in. `.depmap--live` below adds a slow
   breath on top of this for the perpetual state (see that block for why the
   two compose in that order). The old rule here was "once, never a loop", and
   that still governs decorative motion everywhere else on the site — but this
   mark and the D.E.B.T. loop carry the argument the page is making, so they
   are the two deliberate exceptions. BRAND-SYSTEM.md section 7 states the test
   any future loop has to pass. Do not "restore" this to a single pulse. */
.js .depmap--draw .depmap-core {
  transform-box: fill-box;
  transform-origin: center;
  animation: gt-pulse 1.5s var(--ease) 1.3s 1 both;
}
.js .depmap--draw .depmap-cap { animation: gt-fade 0.6s var(--ease) 1.5s both; }

@keyframes gt-draw { from { stroke-dashoffset: 420; } to { stroke-dashoffset: 0; } }
@keyframes gt-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes gt-pulse {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.28); }
  100% { transform: scale(1); }
}

/* ---- hero dependency map: the perpetual inward pulse ---------------------
   The map used to draw once and freeze. It now keeps breathing, and the motion
   says the thing the mark says: pulses of dependency travel ALONG each spoke
   INWARD to the founder square. Everything flows to you. That is the argument
   of the practice, so it is worth animating and worth animating literally.

   Direction. Every spoke is authored core-first in the markup, so path
   distance 0 is the founder square and the outer node is at 98.8. A dash is
   painted where (distance + offset) falls inside the dash length, which means
   a RISING offset walks the dash toward the path start. Inward is therefore
   -122 -> +22 on a `22 340` pattern: at -122 the dash sits past the outer node
   and off the path, at +22 it has slid entirely into the core. The 340 gap is
   larger than the 144 units of travel, so only ever one pulse is in flight per
   spoke. The node squares paint after this group, so each pulse emerges from
   underneath its square rather than popping into existence beside it.

   Pacing. Cycles run 4.6s to 6.8s, all inside the 4-7s calm band, and weight
   drives cadence: the two heavy spokes come round every 4.6s, the two mid
   spokes every 5.4s, and the one light spoke every 6.8s. Heavier dependency
   pulses more insistently, which is the same information the stroke weight
   already carries, said a second way.

   Never in unison. The five delays are scattered rather than sequential, so
   the first pass does not read as a clock hand sweeping the dial, and because
   the three durations are mutually indivisible the phase relationship keeps
   drifting instead of resynchronising.

   Handoff. Delays start at 2.05s, just after the load draw-in finishes its
   last node at 1.71s, and each pulse's first frame is opacity 0. There is
   nothing to snap: the draw-in ends on a complete mark and the first traveller
   fades up on top of it.
   ------------------------------------------------------------------------- */
.js .depmap--live .depmap-trace {
  animation: gt-flow-in 5.4s var(--ease) infinite both;
}
.js .depmap--live .depmap-trace:nth-of-type(1) { animation-duration: 4.6s; animation-delay: 2.05s; }
.js .depmap--live .depmap-trace:nth-of-type(2) { animation-duration: 4.6s; animation-delay: 3.30s; }
.js .depmap--live .depmap-trace:nth-of-type(3) { animation-duration: 5.4s; animation-delay: 4.55s; }
.js .depmap--live .depmap-trace:nth-of-type(4) { animation-duration: 5.4s; animation-delay: 2.70s; }
.js .depmap--live .depmap-trace:nth-of-type(5) { animation-duration: 6.8s; animation-delay: 3.95s; }

/* Cadence from the DATA, for a map built from a real set of scores. The five
   nth-of-type rows above encode the home page's illustrative weights by
   position; a funnel report has no fixed positions, so its traces carry a
   weight class and the duration comes off that instead. Heavy spoke (a low
   score, a hard dependency) cycles fastest, which means the rhythm of the
   visitor's own map is his own dependency structure, said a second way.

   Specificity note, and it is load-bearing: the nth-of-type rows above are
   (0,4,0) because of the pseudo-class. These are written as
   `.depmap-trace.depmap-trace--wN` to reach the same (0,4,0) and placed after,
   so they win on source order. Only animation-duration is set, so the
   scattered delays above still apply and the spokes still never fall into
   step. The five durations share no common factor. */
.js .depmap--live .depmap-trace.depmap-trace--w10 { animation-duration: 4.3s; }
.js .depmap--live .depmap-trace.depmap-trace--w8  { animation-duration: 4.9s; }
.js .depmap--live .depmap-trace.depmap-trace--w7  { animation-duration: 5.5s; }
.js .depmap--live .depmap-trace.depmap-trace--w4  { animation-duration: 6.1s; }
.js .depmap--live .depmap-trace.depmap-trace--w2  { animation-duration: 6.7s; }

/* The founder core keeps the single arrival pulse it always had, then settles
   into a slow breath. Two animations on one property, so order matters: the
   breath is listed second and wins wherever both are live. It carries
   `forwards` rather than `both` deliberately — a backwards fill would let the
   breath's 0% state override the arrival pulse during the pulse's own run. */
.js .depmap--live .depmap-core {
  transform-box: fill-box;
  transform-origin: center;
  animation: gt-pulse 1.5s var(--ease) 1.3s 1 both,
             gt-breathe 6.4s var(--ease) 2.9s infinite forwards;
}

@keyframes gt-flow-in {
  0%   { stroke-dashoffset: -122; opacity: 0; }
  10%  { opacity: 1; }
  66%  { opacity: 1; }
  80%  { stroke-dashoffset: 22; opacity: 0; }
  100% { stroke-dashoffset: 22; opacity: 0; }
}
@keyframes gt-breathe {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.07); }
  100% { transform: scale(1); }
}


/* ---- the D.E.B.T. loop: four phases, forever -----------------------------
   D -> E -> B -> T -> D. One 9.6s cycle, 2.4s a phase, and the list row and the
   diagram box for the same phase light together off the same delay. The whole
   thing is animation-delay staggering over five keyframe sets; there is no JS
   involved and no state to keep in sync.

   The window inside gt-loop-* is: ramp up 0-7% (0.67s), hold 7-19% (1.15s),
   ramp down 19-28% (0.86s), rest 28-100%. The ramp down deliberately runs two
   points past the 25% handoff, so for about 0.3s the outgoing phase is still
   fading while the incoming one is rising and the connector between them is
   carrying a pulse. That overlap is what makes the eye travel the path instead
   of teleporting between corners.

   Nothing snaps: every phase starts and ends the cycle in its resting state,
   which is also its declared state, so a dropped animation leaves four legible
   boxes rather than one arbitrarily lit.

   The connectors use the same dash-traveller mechanism as the dependency map,
   but authored source-first, so here the pulse runs toward the path END and
   the offset falls (+18 -> -126) instead of rising. Each fires at the tail of
   the phase it leaves: de at 0s, eb at 2.4s, bt at 4.8s, td at 7.2s.

   The gold return arrow, Transfer back to Diagnose, is the beat the whole
   diagram exists for, so it is the one that is different: a longer dash on a
   heavier stroke, a travel window half again as long as the other three, and
   an arrowhead that blooms wider when it lands.
   ------------------------------------------------------------------------- */
.js .loop--cycle .loop-box    { animation: gt-loop-box 9.6s var(--ease) infinite both; }
.js .loop--cycle .loop-letter { animation: gt-loop-letter 9.6s var(--ease) infinite both; }
.js .loop--cycle .loop-name   { animation: gt-loop-name 9.6s var(--ease) infinite both; }
.js .loop--cycle .loop-arrow  { animation: gt-loop-arrow 9.6s var(--ease) infinite both; }
.js .loop--cycle .loop-trace  { animation: gt-loop-trace 9.6s var(--ease) infinite both; }
.js .debt-list--cycle .debt-item   { animation: gt-debt-row 9.6s var(--ease) infinite both; }
.js .debt-list--cycle .debt-letter { animation: gt-debt-letter 9.6s var(--ease) infinite both; }
.js .debt-list--cycle .debt-name   { animation: gt-debt-name 9.6s var(--ease) infinite both; }

/* The one stagger table. Every selector on a row shares a delay, and that is
   the whole synchronisation contract between the list and the diagram.

   Each row is written to exactly the same specificity (0,3,0) as the eight
   `animation:` shorthands above and sits after them, so it wins on source
   order. That is not incidental: the shorthand resets animation-delay to 0s,
   so a stagger rule that scored lower would be silently overwritten and all
   four phases would light at once. */
.js .loop--cycle .loop-phase--d *,
.js .debt-list--cycle .debt-item--d,
.js .debt-list--cycle .debt-item--d * { animation-delay: 0s; }
.js .loop--cycle .loop-phase--e *,
.js .debt-list--cycle .debt-item--e,
.js .debt-list--cycle .debt-item--e * { animation-delay: 2.4s; }
.js .loop--cycle .loop-phase--b *,
.js .debt-list--cycle .debt-item--b,
.js .debt-list--cycle .debt-item--b * { animation-delay: 4.8s; }
.js .loop--cycle .loop-phase--t *,
.js .debt-list--cycle .debt-item--t,
.js .debt-list--cycle .debt-item--t * { animation-delay: 7.2s; }
/* a connector fires as focus leaves the phase it starts from */
.js .loop--cycle .loop-trace--de, .js .loop--cycle .loop-arrow--de { animation-delay: 0s; }
.js .loop--cycle .loop-trace--eb, .js .loop--cycle .loop-arrow--eb { animation-delay: 2.4s; }
.js .loop--cycle .loop-trace--bt, .js .loop--cycle .loop-arrow--bt { animation-delay: 4.8s; }
.js .loop--cycle .loop-trace--td, .js .loop--cycle .loop-arrow--gold { animation-delay: 7.2s; }
/* the payoff beat overrides the shared trace + arrowhead behaviour */
.js .loop--cycle .loop-trace--td { animation-name: gt-loop-trace-gold; }
.js .loop--cycle .loop-arrow--gold { animation-name: gt-loop-arrow-gold; }

/* The five phase keyframes all share one window: rest -> 10% -> hold to 19%
   -> rest by 31%. Those percentages are not evenly spaced by accident. The
   shared easing is a strong ease-out, so within any segment roughly 95% of
   the change lands in the first two thirds of the time; a 9-point ramp reads
   as about a 0.6s fade rather than a 1.1s one. The numbers are picked so the
   perceived fade is soft on both ends and the tail of one phase still overlaps
   the head of the next, which is what stops the loop reading as a switch. */
@keyframes gt-loop-box {
  0%, 100% { stroke: var(--loop-rest); stroke-width: 1.25; fill: var(--loop-lift-0); }
  10%, 19% { stroke: var(--loop-active); stroke-width: 2; fill: var(--loop-lift); }
  31%      { stroke: var(--loop-rest); stroke-width: 1.25; fill: var(--loop-lift-0); }
}
@keyframes gt-loop-letter {
  0%, 100% { fill: var(--loop-rest); }
  10%, 19% { fill: var(--loop-active); }
  31%      { fill: var(--loop-rest); }
}
@keyframes gt-loop-name {
  0%, 100% { fill: var(--loop-rest-name); }
  10%, 19% { fill: var(--loop-active-name); }
  31%      { fill: var(--loop-rest-name); }
}
@keyframes gt-debt-row {
  0%, 100% { border-bottom-color: var(--hairline); }
  10%, 19% { border-bottom-color: var(--loop-active); }
  31%      { border-bottom-color: var(--hairline); }
}
@keyframes gt-debt-letter {
  0%, 100% { color: var(--loop-rest-name); }
  10%, 19% { color: var(--loop-active); }
  31%      { color: var(--loop-rest-name); }
}
@keyframes gt-debt-name {
  0%, 100% { color: var(--loop-rest-dim); }
  10%, 19% { color: var(--loop-active-name); }
  31%      { color: var(--loop-rest-dim); }
}

/* The travel windows are wide (15-37%, and 15-45% for the return) for the same
   ease-out reason, and this one was measured rather than guessed. A dash is on
   the path only while its offset is between +dash and -(length + dash), which
   for these connectors is the first 43% of the offset range; under the shared
   curve that 43% of DISTANCE is covered in the first 43% of the SEGMENT, so a
   1.2s travel window put the pulse across the connector in half a second and
   then held an invisible element at opacity 1 for the rest. Widening the
   window to 2.1s puts a readable ~0.95s crossing on screen. The opacity ramps
   are set to the measured crossing, so the pulse fades in as it leaves the box
   and is gone by the time it is off the far end. */
@keyframes gt-loop-trace {
  0%, 15% { stroke-dashoffset: 18; opacity: 0; }
  18%     { opacity: 1; }
  24%     { opacity: 1; }
  28%     { opacity: 0; }
  37%     { stroke-dashoffset: -126; }
  100%    { stroke-dashoffset: -126; opacity: 0; }
}
/* the payoff: a longer dash on a heavier stroke, travelling for half again as
   long as the other three, landing just as Diagnose comes back up */
@keyframes gt-loop-trace-gold {
  0%, 15% { stroke-dashoffset: 24; opacity: 0; }
  18%     { opacity: 1; }
  26%     { opacity: 1; }
  30%     { opacity: 0; }
  45%     { stroke-dashoffset: -132; }
  100%    { stroke-dashoffset: -132; opacity: 0; }
}
@keyframes gt-loop-arrow {
  0%, 20%, 100% { fill: var(--loop-rest); transform: scale(1); }
  26%           { fill: var(--loop-active); transform: scale(1.45); }
  34%           { fill: var(--loop-rest); transform: scale(1); }
}
@keyframes gt-loop-arrow-gold {
  0%, 20%, 100% { transform: scale(1); }
  29%           { transform: scale(1.8); }
  40%           { transform: scale(1); }
}

/* ---- a diagram that draws itself once ------------------------------------
   `.loop--draw` is the one-shot twin of `.loop--cycle`, for a figure that
   illustrates a state rather than a cycle: methodology's stage-0-to-stage-3
   transfer diagram, where the sentence beside it is "every loop adds more
   gold" and the gold square is what that sentence points at.

   Decoration pulses once; only the two perpetual loops loop. This is the
   "once" case, and the single pulse lands on the gold square alone.

   It is triggered by REVEAL rather than by load, because these figures are
   below the fold: the rule only starts applying when an ancestor picks up
   `.is-revealed` from the existing observer, and an animation begins when its
   rule begins to apply. No new JS, and a figure that is never revealed simply
   rests in its declared state, which is the finished one. The self hook is
   there for a figure that carries `.reveal` on the <figure> itself. */
.js .is-revealed .loop--draw .loop-line,
.js .loop--draw.is-revealed .loop-line {
  stroke-dasharray: 420;
  animation: gt-draw 0.95s var(--ease) both;
}
.js .is-revealed .loop--draw .loop-line:nth-of-type(1),
.js .loop--draw.is-revealed .loop-line:nth-of-type(1) { animation-delay: 0.05s; }
.js .is-revealed .loop--draw .loop-line:nth-of-type(2),
.js .loop--draw.is-revealed .loop-line:nth-of-type(2) { animation-delay: 0.14s; }
.js .is-revealed .loop--draw .loop-line:nth-of-type(3),
.js .loop--draw.is-revealed .loop-line:nth-of-type(3) { animation-delay: 0.23s; }
.js .is-revealed .loop--draw .loop-line:nth-of-type(4),
.js .loop--draw.is-revealed .loop-line:nth-of-type(4) { animation-delay: 0.32s; }
.js .is-revealed .loop--draw .loop-line:nth-of-type(5),
.js .loop--draw.is-revealed .loop-line:nth-of-type(5) { animation-delay: 0.41s; }
.js .is-revealed .loop--draw .loop-line:nth-of-type(6),
.js .loop--draw.is-revealed .loop-line:nth-of-type(6) { animation-delay: 0.50s; }
.js .is-revealed .loop--draw .loop-line:nth-of-type(7),
.js .loop--draw.is-revealed .loop-line:nth-of-type(7) { animation-delay: 0.59s; }
.js .is-revealed .loop--draw .loop-box,
.js .loop--draw.is-revealed .loop-box {
  animation: gt-fade 0.5s var(--ease) 0.72s both;
}
.js .is-revealed .loop--draw .loop-arrow--gold,
.js .loop--draw.is-revealed .loop-arrow--gold {
  transform-box: fill-box;
  transform-origin: center;
  animation: gt-pulse 1.5s var(--ease) 1.05s 1 both;
}


/* =========================================================================
   13. RESPONSIVE
   -------------------------------------------------------------------------
   Breakpoints, in descending order: 1100 / 900 / 860 / 760 / 640 / 430.

   Two were added at this pass and the reasoning is worth keeping.

   900 is now the TOUCH RAMP. The tap-target rules used to live at 760, which
   meant that at exactly 768 — iPad portrait, a device with no pointer at all —
   the desktop nav was still live with 18.7px links, a 36px CTA and 33.8px
   footer links. Fifteen to twenty sub-44px targets per page at a width that is
   unambiguously a finger. The ramp is width-based rather than
   `(pointer: coarse)` because a coarse-pointer query misses a touchscreen
   laptop and a hybrid, and a 900px browser window getting comfortable targets
   is not a defect.

   860 is the hamburger. The desktop row still fits between 861 and 900, so
   the two are deliberately not the same number: the tap ramp starts as soon as
   the width is plausibly a tablet, the layout change waits until the row is
   actually tight.

   430 is the narrow-phone composition. Everything above it is a layout that
   collapses; 430 down is a layout that was designed.
   ========================================================================= */
@media (max-width: 1100px) {
  .split { gap: 48px; }
  .split--rule > :last-child { padding-left: 40px; }
  .footer-grid { gap: 36px; }
}

@media (max-width: 900px) {
  .section { padding: 72px 0; }
  .hero { padding: 72px 0 80px; }
  .hero-grid { grid-template-columns: minmax(0, 1fr); gap: 48px; }
  .hero h1 { max-width: none; }
  /* Centred, not left-flush. Between 641 and 900 the map drops below the
     copy into a full-width ink column and used to sit at its natural size
     against the left gutter, leaving a wide band of empty ink to its right —
     the one width where it read as placed rather than composed. It is
     full-bleed centred at 390 and beside the copy at 900+, so centring here
     is what makes the three states one composition. */
  .hero-visual { max-width: 360px; justify-self: center; }
  .split { grid-template-columns: minmax(0, 1fr); gap: 40px; }
  .split--rule > :last-child { border-left: none; padding-left: 0; }
  .cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* .cols-3 steps here too, not straight from 3-up to 1-up at 760. Three cells
     inside a 1040px wrap at 900px are ~215px wide, which wraps a serif pull
     quote at four words a line. The contract's "3-col -> 1-col at 760" was
     written before the three-up grids carried prose. */
  .cols-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* ...and a three-cell set stepping to two columns leaves the fourth track
     VACANT, where `.grid-hairline`'s own `background: var(--line)` paints a
     solid grey rectangle the size of a tile. It does not read as whitespace,
     it reads as a card that failed to load — measured at 768 on index's
     Bottleneck Tax tiles, index's testimonial row and about's three columns.
     Spanning the last cell full-width fills the row and keeps the hairline
     grid honest. Only fires when the third cell is also the last, so a 5- or
     6-cell `.cols-3` is untouched. */
  .cols-3 > *:nth-child(3):last-child,
  .cols-4 > *:nth-child(3):last-child { grid-column: 1 / -1; }
  .footer-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 40px; }
  .footer-brand { grid-column: 1 / -1; }
}

/* -------------------------------------------------------------------------
   THE TOUCH RAMP. Every affordance reaches 44px here, not at 760.
   Measured before: .btn-text 40.8, nav .btn--sm 40 (36 at 768), .link-quiet
   26, .nav-logo 28, footer links 33.8 at 768.
   ------------------------------------------------------------------------- */
@media (max-width: 900px) {
  .nav-link, .footer-col a { display: block; padding: 12px 0; }
  .nav-link--plain { border-bottom: none; }
  .btn { padding: 15px 28px; }
  .btn--sm { padding: 15px 20px; }        /* 10px caps + 30 + border = 44 */
  .btn-text { padding: 17px 0 5px; }
  .link-quiet { display: inline-block; padding: 9px 0; }
  .tap { display: inline-block; padding: 13px 0; }

  /* A link inside a sentence is exempt from the target-size rule precisely
     because padding it out reflows the paragraph around it. Negative margins
     equal to the padding are the way to have both: the margin box stays the
     height of one line so the line box does not grow, while the padding box —
     which is what the finger hits — is 47px tall. Layout is byte-identical
     before and after; only the hit area changes. Standalone links (privacy's
     contact line, a mailto CTA) should take `.tap` instead and get a real box.
     A `.tap-inline` opt-in twin of this rule was deleted at fix time with zero
     uses; the mailto selector is what every page relies on. */
  main a[href^="mailto:"] {
    display: inline-block;
    padding: 10px 0;
    margin: -10px 0;
  }
}

@media (max-width: 860px) {
  /* the hamburger owns the nav from here down, so the CTA is free to wrap
     again rather than overflow a 320px panel */
  .nav-menu .btn { white-space: normal; }

  /* ---- mobile nav ------------------------------------------------------
     Without JS the hamburger cannot open anything, so it stays hidden and
     the link row is simply allowed to wrap. With JS the row becomes a
     full-width panel under the bar and the hamburger appears.
     js/site.js resizes against the same 860; keep the two in step. */
  .nav-menu { flex-wrap: wrap; row-gap: 4px; column-gap: 20px; }
  .js .nav-toggle { display: inline-flex; }
  .js .nav-menu {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    padding: 12px 28px 22px;
    max-height: calc(100vh - 100%);
    max-height: calc(100dvh - 100%);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  .js .nav-menu.is-open { display: flex; }
  .js .nav-menu .nav-link {
    border-bottom: 1px solid var(--line);
    padding: 15px 0;
    text-align: left;
  }
  .js .nav-menu .nav-link--plain { font-size: 15px; }
  .js .nav-menu .btn { margin-top: 18px; align-self: flex-start; }
}

@media (max-width: 760px) {
  .section { padding: 64px 0; }
  .section--tight { padding: 48px 0; }
  .cols-3 { grid-template-columns: minmax(0, 1fr); }
  .spec-row { grid-template-columns: minmax(0, 1fr); gap: 8px; }
  .grid-hairline > * { padding: 30px 24px; }
  .section-head { margin-bottom: 40px; }
  .gauges { gap: 32px; }
  .split--panel > :last-child { padding: 26px 24px; }
}

@media (max-width: 640px) {
  body { font-size: 16px; }
  .wrap, .wrap-wide { padding: 0 20px; }
  .js .nav-menu { padding-left: 20px; padding-right: 20px; }
  .nav-inner { padding: 14px 20px; }
  .section { padding: 56px 0; }
  .section--tight { padding: 40px 0; }
  .hero { padding: 56px 0 64px; }
  .hero-sub { margin: 22px 0 34px; }
  .cols-2, .cols-4 { grid-template-columns: minmax(0, 1fr); }
  .split--rule > :last-child { padding-left: 0; }
  .debt-item { grid-template-columns: 28px minmax(0, 1fr); gap: 14px; }
  .steps > li { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .pull { padding-left: 20px; }
  .actions { gap: 14px; }
  .actions .btn { width: 100%; }
  .footer-grid { grid-template-columns: minmax(0, 1fr); gap: 32px; }
  .footer-col ul { display: grid; }

  /* ---- the content type ramp -------------------------------------------
     Principle 4 sets a 16px floor for BODY. The split that matters is
     content versus chrome: a 13px sub-question a visitor is meant to read
     and answer is content and has to come up; an 11px mono kicker is a
     system label and is correct where it is. So `.fine`, `.note`,
     `.stat-note`, `.stage-note`, `.spec-v` and `.debt-note` all step up
     here, and `.kicker`, `.label`, `.figure-cap`, `.trole`, `.gauge-cap`
     and `.spec-k` are deliberately untouched.

     This costs vertical space, which is one more argument for the phone
     compositions below rather than against the ramp. */
  .fine { font-size: 15px; }
  /* `.note` carries diagnostic.html's seventeen five-area sub-questions —
     full sentences the visitor is asked to read and answer. Those are
     content, so they take the 16px floor the comment above cites, not the
     15px they shipped with. `.fine` stays at 15 because it is genuine fine
     print (the illustrative-figure disclaimers), which is chrome. */
  .note, .stat-note, .stage-note { font-size: 16px; }
  .spec-v, .debt-note, .step-body p { font-size: 16px; }
  .faq-a { font-size: 16px; }
  /* 10px mono at 0.08em wrapped the longer job titles at 430 */
  .trole { letter-spacing: 0.04em; }

  /* ---- gauges on the phone ----------------------------------------------
     118px with an 8px arc and a 28px number is a thumbnail of a gauge. At
     150 the pair still fits side by side inside a 390 viewport (150 + 150 +
     20 gutter-to-gutter = 320 against 350 of content), the arc reads, and
     the number lands at 31px. The signature visual survives the phone at
     full strength rather than being shrunk out of the argument. */
  .gauges { gap: 20px; }
  .gauge-svg { width: 150px; height: 150px; }
  .gauge--sm .gauge-svg { width: 118px; height: 118px; }

  /* ---- the zebra run ---------------------------------------------------- */
  .grid-hairline--zebra > *:nth-child(even) { background: var(--fill-alt); }

  /* ---- the stage ladder -------------------------------------------------
     A four-up ladder collapsed to four stacked cells stops being a ladder:
     read one at a time, Trapped and Transferred are the same cell with
     different words in it, and the whole point of the component is the
     progression between them.

     The phone device is a left rule that THICKENS stage by stage. It is the
     methodology page's own metaphor made literal — "every loop adds more
     gold" — drawn with the two primitives the system already owns, so it
     adds no visual language. The accent moves onto the rule, which is why
     the mono stage label steps back to the meta ramp here: one gold accent
     per composition, and the rule is now carrying it. */
  .grid-hairline > .stage {
    border-left: 2px solid var(--accent);
    padding-left: 22px;
  }
  .grid-hairline > .stage:nth-child(2) { border-left-width: 5px; }
  .grid-hairline > .stage:nth-child(3) { border-left-width: 9px; }
  .grid-hairline > .stage:nth-child(4) { border-left-width: 14px; }
  .grid-hairline > .stage .stage-n { color: var(--text-dim); }

  /* ---- testimonial measure ---------------------------------------------
     17px serif at 350px runs about 40 characters a line, a third short of
     the 50-70 measure, and turns three quotes into 1137px. 16px on tighter
     leading is the honest fix at this width; it stays on the 16px body
     floor. */
  .tquote { font-size: 16px; line-height: 1.55; }

  /* the panel keeps its floor on the phone: that is the whole component */
  .split--panel > :last-child { padding: 24px 20px; }
}

/* The index D.E.B.T. loop is a 420-unit viewBox and was never given the label
   rescue its 640-unit twin on methodology got, so at 390 its DIAGNOSE /
   EXTRACT / BUILD / TRANSFER names and the "THE PROGRESSIVE LOOP" caption
   rendered at 7.9px — the signature 9.6s cycle legible as shapes with
   unreadable labels on the primary device.

   `:not(.loop-svg--wide)` is required: the wide rescue earlier in this file matches
   `.loop-svg` too, and this rule would otherwise fight it. At 390 the figure
   renders at 350 of a 420 box (scale 0.833), so 12.5 units comes out at
   10.4px; at 430 it is 11.6px. */
@media (max-width: 440px) {
  .loop-svg:not(.loop-svg--wide) .loop-name,
  .loop-svg:not(.loop-svg--wide) .loop-cap { font-size: 12.5px; }
}

/* -------------------------------------------------------------------------
   430 AND BELOW — the narrow-phone composition.

   This is the primary device. The distribution channel is LinkedIn and the
   real fold with browser chrome is about 750px, not the 844 or 932 the
   viewport reports. Everything here exists to put the argument above that
   line, and the rule the block follows is: shrink the frame, never the mark.
   ------------------------------------------------------------------------- */
@media (max-width: 430px) {
  /* Buying back the home hero. At 390 the dependency map ran 630 to 912 and
     its caption — "Every line leads to you.", the sentence the whole mark
     exists to earn — sat at 895, so the signature perpetual pulse never
     shared a frame with the headline it argues for. Nothing here is hidden
     or scaled down to nothing: the padding, the label gaps and the column
     gap give the space back, and the map itself only steps from 350 to 300. */
  .hero { padding: 32px 0 44px; }
  .hero .kicker { margin-bottom: 16px; }
  .hero-sub { margin: 18px 0 26px; }
  .hero-grid { gap: 24px; }
  .hero-visual { max-width: 300px; }
  .depmap-cap { margin-top: 14px; }
  /* The mark arrives WITH the headline instead of 630px below it. Reordering
     a grid item is the whole change: the DOM stays copy-then-visual, so the
     reading order a screen reader and a keyboard get is unchanged, and the
     map's own <title> plus the .visually-hidden description still sit where
     they were authored. Above 430 the order is untouched. */
  .hero-visual { order: -1; }

  /* At 300px the 340-unit viewBox scales to 0.88, which renders the 9-unit
     area labels at 7.9px. SVG font-size is in user units and a CSS rule
     out-specifies a presentation attribute, so the declared size steps up
     and the RENDERED size lands at 9.7px. 11 units is the ceiling: at 12 the
     "DELIVERY" string, anchored at x=274 in a 340-wide box, runs past the
     viewBox edge. Verify rendered pixels, never declared ones. */
  .depmap-label { font-size: 11px; }

  .section { padding: 48px 0; }
  .section--tight { padding: 36px 0; }
  .section-head { margin-bottom: 32px; }
  .grid-hairline > * { padding: 26px 20px; }
  .grid-hairline > .stage { padding-left: 18px; }

  /* THE PRIMARY CTA, at the width that actually breaks it. The label is
     contract-mandated copy ("Take the Founder Bottleneck Score(tm)") so the
     fix is type, not words. At 390 the 12px/0.14em `.btn--lg` measured just
     over the content box and stranded "SCORE(tm)" alone on a second line,
     centred, on all four instances (index hero, index closing band,
     methodology closing band, about closing CTA). 11px at 0.07em brings the
     label back inside one line at 390 and 412, and where it does wrap at
     narrower widths it breaks two-and-two rather than orphaning. `.btn--lg`
     is the only size touched: `.btn` and `.btn--sm` carry shorter labels and
     hold their tracking. */
  .btn--lg { font-size: 11px; letter-spacing: 0.07em; padding: 17px 24px; }
}

/* -------------------------------------------------------------------------
   360 AND BELOW. The iPhone SE viewport, under every width the principles
   list, but still a real device and still the only place any h1 breaks
   badly: at 320 the h1 clamp floors at 38px, "Bottleneck Score" needs about
   304px against a 280px content box, and diagnostic.html's headline went to
   three lines with "Score(tm)" alone on the last. Dropping the floor to 32px
   fits the two-word second line. Verified at 320 across all six h1s: index,
   methodology, about, privacy and 404 already broke cleanly and still do.
   ------------------------------------------------------------------------- */
@media (max-width: 360px) {
  h1 { font-size: clamp(32px, 9vw, 38px); }
}


/* =========================================================================
   14. REDUCED MOTION
   -------------------------------------------------------------------------
   Kills every animation declared above and resets any state a JS-managed
   animation could have left behind, in case the preference is flipped
   mid-session. site.js also reads matchMedia and skips the observers and the
   count-up outright, so this block is the belt to that pair of braces.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal, .js .reveal {
    opacity: 1 !important;
    transform: none !important;
    /* The blur has to be cleared here too. Without this line a reduced-motion
       visitor gets a permanently out-of-focus page, because the entrance that
       would have removed it never runs. */
    filter: none !important;
    transition: none !important;
  }
  /* The press squash is motion too. A reduced-motion visitor still gets the
     colour change on :hover, which is the part that carries the affordance. */
  .btn:active { transform: none !important; }
  .depmap-line, .js .depmap--draw .depmap-line {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
    animation: none !important;
  }
  .depmap-node, .depmap-core, .depmap-cap,
  .js .depmap--draw .depmap-node,
  .js .depmap--draw .depmap-core,
  .js .depmap--draw .depmap-cap,
  .js .depmap--live .depmap-core {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }

  /* The two perpetual loops. Both are built so that "rest" and "declared" are
     the same state, which means stopping them is enough on its own: the
     dependency map is left fully drawn with a still core, and all four D.E.B.T.
     phases are left at rest rather than one of them arbitrarily lit.

     The travellers are the exception, because their declared state is mid-path
     and invisible only by virtue of opacity 0. They are pinned there. */
  .depmap-trace, .js .depmap--live .depmap-trace,
  .loop-trace, .js .loop--cycle .loop-trace {
    animation: none !important;
    opacity: 0 !important;
  }
  .js .loop--cycle .loop-box,
  .js .loop--cycle .loop-letter,
  .js .loop--cycle .loop-name,
  .js .loop--cycle .loop-arrow,
  .js .debt-list--cycle .debt-item,
  .js .debt-list--cycle .debt-letter,
  .js .debt-list--cycle .debt-name {
    animation: none !important;
    transform: none !important;
  }
  /* Without the loop there is nothing for a dimmed letter to light up to, so
     the list goes back to the all-gold set it had before the loop existed.
     Written at the same (0,3,0) specificity as the rule it undoes in the
     component block, and later in the file, so it wins on source order — a
     media query does not by itself raise specificity. */
  .js .debt-list--cycle .debt-letter { color: var(--accent); }
  .js .band .debt-list--cycle .debt-letter { color: var(--gold); }

  /* ---- the value-bearing components ------------------------------------
     The resting state is the FINISHED value in every case: a full arc at the
     real score, a meter at its real width, the real number. Never a partial
     arc, never a bar caught mid-fill, because a half-drawn quantity is not a
     stopped animation, it is wrong data.

     The gauge needs nothing but a stopped transition: site.js reads the same
     preference and paints the final frame directly instead of animating, and
     with JS off the finished arc is already in the markup as a presentation
     attribute. The meter does need its width restored, because the `.js`
     rule above zeroes it in order to have something to transition. */
  .gauge-arc, .gauge-num--val { transition: none !important; animation: none !important; }
  .depmap-node { transition: none !important; }
  .js .meter-fill {
    width: var(--meter-w, 0%) !important;
    transition: none !important;
  }
  /* the one-shot diagram draw rests fully drawn, gold square unpulsed */
  .js .is-revealed .loop--draw .loop-line,
  .js .loop--draw.is-revealed .loop-line,
  .js .is-revealed .loop--draw .loop-box,
  .js .loop--draw.is-revealed .loop-box,
  .js .is-revealed .loop--draw .loop-arrow--gold,
  .js .loop--draw.is-revealed .loop-arrow--gold {
    animation: none !important;
    transform: none !important;
    opacity: 1 !important;
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
  }

  .nav { transition: none; }
  .btn, .nav-link, a { transition: none !important; }
  * { will-change: auto !important; }
}


/* =========================================================================
   15. PRINT
   -------------------------------------------------------------------------
   Ink grounds do not print their background, so paper-coloured text on them
   comes out invisible. Every dark surface is forced back to dark-on-white,
   the chrome is dropped, and external URLs are appended: a printed page that
   loses its links is one the reader has to come back for.
   ========================================================================= */
@media print {
  body { background: #fff !important; color: #171512 !important; font-size: 11pt; }
  .nav, .nav-toggle, .skip-link, .footer-bottom { display: none !important; }

  .reveal, .js .reveal { opacity: 1 !important; transform: none !important; }
  .depmap-line, .depmap-node, .depmap-core, .depmap-cap {
    animation: none !important;
    opacity: 1 !important;
    stroke-dashoffset: 0 !important;
    stroke-dasharray: none !important;
  }
  /* the two sets of travellers exist only to move; frozen on paper they are a
     stray tick across a diagram, so they do not print at all */
  .depmap-trace, .loop-trace { display: none !important; }
  .loop-box, .loop-letter, .loop-name, .loop-arrow,
  .debt-item, .debt-letter, .debt-name {
    animation: none !important;
    transform: none !important;
  }
  .debt-list--cycle .debt-letter { color: #5d4720 !important; }

  .hero, .band, .footer, .cell--ink,
  .hero h1, .hero p, .hero .lede, .hero-sub,
  .band h1, .band h2, .band h3, .band p, .band .lede,
  .cell--ink p, .cell--ink .stage-name, .cell--ink .stage-note,
  .footer p, .footer a {
    background: #fff !important;
    color: #171512 !important;
  }
  .hero .kicker, .band .kicker, .cell--ink .stage-n,
  .band .label, .footer-col h2, .label--accent, .kicker {
    color: #5d4720 !important;
  }
  .band .depmap-line, .depmap-line { stroke: #5d4720 !important; }
  .band .depmap-node, .depmap-node { fill: #fff !important; stroke: #171512 !important; }
  /* ...but a founder-dependent node keeps its fill. The line weight also
     encodes it, but the fill is the primary read on screen, and a printed map
     that flattens every node to white has lost the one thing the diagram is
     saying. Same argument as the `.is-low` / `.is-high` value inks above. */
  .band .depmap-node--dep, .depmap-node--dep { fill: #5d4720 !important; }
  .band .depmap-core, .depmap-core { fill: #5d4720 !important; stroke: #171512 !important; }
  .band .loop-line, .loop-line { stroke: #171512 !important; }
  .band .loop-arrow, .loop-arrow { fill: #171512 !important; }
  .band .loop-box, .loop-box { stroke: #171512 !important; }
  .band .loop-letter, .loop-letter { fill: #171512 !important; }
  .band .loop-name, .loop-name { fill: #4a4740 !important; }
  /* The value language SURVIVES the printer. Flattening `.is-low` and
     `.is-high` to one ink would strip the report of the only thing its
     colours were saying — low is a constraint, high has cleared the line —
     on the copy most likely to be handed to somebody else. The two print
     inks are the darkened accent and the body ink, both of which read on
     white and are distinguishable in greyscale by value. */
  .is-low, .is-gold { --val: #5d4720 !important; }
  .is-high, .is-paper { --val: #171512 !important; }
  .band .gauge-num, .gauge-num { fill: #171512 !important; }
  .gauge-num--val { fill: var(--val, #171512) !important; }
  .gauge-track { stroke: #d5d1c8 !important; }
  .gauge-arc { stroke: var(--val, #5d4720) !important; transition: none !important; }
  .meter { color: var(--val, #171512) !important; }
  .meter-track { background: #d5d1c8 !important; }
  .meter-fill {
    background: currentColor !important;
    width: var(--meter-w, 0%) !important;
    transition: none !important;
  }
  .meter-name { color: #171512 !important; }
  .meter-tag { color: #5d4720 !important; }
  .track-seg { background: #d5d1c8 !important; }
  .track-seg.is-on { background: #5d4720 !important; }
  .iconbox { color: #5d4720 !important; border-color: #171512 !important; }
  .loop-line, .loop-box, .loop-arrow--gold {
    animation: none !important;
    opacity: 1 !important;
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
    transform: none !important;
  }
  .depmap-node--empty { fill: #fff !important; stroke: #4a4740 !important; }

  /* The footer wordmark is an <img> of a flat fill="#f5f2ea" SVG, so CSS
     cannot recolour its interior and the print block's white footer ground
     would print it cream on white. Inverting the raster is the one lever a
     referenced image leaves. */
  .footer-brand img { filter: invert(1); }

  .section, .section--tight, .hero, .footer { padding: 18px 0 !important; }
  .grid-hairline { border-color: #171512 !important; background: #171512 !important; }
  .grid-hairline > * { background: #fff !important; }
  .card, .tcard, .grid-hairline > *, .spec-row {
    break-inside: avoid;
    page-break-inside: avoid;
  }
  h1, h2, h3 { break-after: avoid; page-break-after: avoid; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; color: #4a4740; }
  a[href^="mailto"]::after, a[href^="#"]::after { content: ""; }
}
