/* Hallmark · pre-emit critique: P5 H5 E4 S5 R5 V5
 * Hallmark · macrostructure: modern-minimal (concept C) — sticky bar, centred
 *   fold, card sections, a proof bar, a before/after
 *   panel, a quiet colophon
 * · genre: modern-minimal · theme: concept-C · tone: current, unhurried
 * · nav: bespoke masthead (~N6) · footer: bespoke colophon (~Ft4)
 * · enrichment: none (typography and press rules only) · anchor hue: warm 32
 * · studied: yes · DNA-source: url
 * · source-url: https://www.usehallmark.com/examples/custom-04/
 * · fonts: Instrument Sans + Instrument Serif (one italic word) + JetBrains Mono
 * · observed-paper: oklch(94% 0.012 85) · observed-accent: oklch(58% 0.205 32)
 * · rhythm: unknown (URL mode) — this page's rhythm is its own
 * · audience: owner/marketer at a small B2B firm, sceptical of SEO vendors
 * · use case: decide whether to book one audit
 *
 * D22 landed on the broadsheet. D45a replaced it with concept C, and kept
 * the broadsheet where it still earns its keep: `templates/report.css`, the
 * RENDERED AUDIT. That split is the decision, not a compromise. A marketing
 * page is read by somebody deciding in eight seconds and has to look
 * current. A report is read by somebody checking a number and has to look
 * like a document that was signed. They are different jobs and the
 * broadsheet only ever did one of them well.
 *
 * Carried forward from the broadsheet, because they were never about the
 * newsprint: the accent split by contrast duty (a display red and a red
 * that has words in it), three outcomes rendered as three things rather
 * than as pass/fail, and colour never carrying meaning alone.
 *
 * Dropped: uppercase headings, the crop marks, the 3px press rules, the
 * hand-numbered broken grid. Uppercase in particular was a broadsheet
 * mannerism — it costs word-shape, which is the thing a reader scans by.
 */

/* tokens · palette supplied 2026-09-07 (coolors.co/e63946-f1faee-a8dadc-457b9d-1d3557)
 *
 * Only the colour block below changed. Type, space and rules are untouched,
 * so this is a palette swap and nothing else — which is what `page.css`
 * consuming names rather than values was for.
 *
 * Three of the five supplied colours ship exactly as given: the paper
 * (#f1faee), the navy ink (#1d3557), and the red (#e63946). The other two
 * are darkened, and the amount is arithmetic rather than taste: every token
 * a reader reads has to clear 4.5:1 against the *darkest* surface it can sit
 * on, not just against the paper, so no combination of tokens can produce
 * text nobody can read. `scripts/check_contrast.py` fails the build if that
 * stops being true.
 *
 * The accent keeps the split by contrast DUTY that the previous theme
 * introduced, because the supplied red makes the case for it: #e63946 is
 * 3.90:1 on this paper. That clears AA for large text and fails it for small
 * — so the red as supplied is the display red, and a darker one does the
 * work that has words in it. Using the supplied red for a button label would
 * have been the flattering choice and an unreadable one.
 *
 * The blue earning `--color-inconclusive` is the useful accident in this
 * palette. The rubric has three outcomes and a report that renders
 * "inconclusive" as a failure is lying; a palette that hands us a blue
 * plainly unrelated to the red makes that distinction visible rather than
 * merely stated.
 */

:root {
  /* ── Paper and ink · EMBER ─────────────────────────────────
     D80. The palette changed and the names did not, which is the whole
     reason the names are what they are. `--color-paper` has never meant
     "white": it means the ground a reader reads on. It is now a deep plum,
     and every rule in page.css that referred to it kept working without
     being touched.

     The previous palette (#f1faee paper, #1d3557 ink, #e63946 accent,
     #a8dadc powder, #457b9d steel) is not recorded here. It is in the
     decision register and in git, which is where a replaced palette
     belongs; a list of last year's colours in the file that defines this
     year's is how two palettes end up half-applied.

     Every value below was measured against the three surfaces before it
     was written, not after. `scripts/check_contrast.py` holds them. */
  --color-paper:      oklch(21.5% 0.052 347);    /* #2A0E1E · the base plum · the ground */
  --color-paper-2:    oklch(24.9% 0.059 348);    /* #351426 · one step up   · tinted band */
  --color-paper-3:    oklch(28.1% 0.068 350);    /* #41192E · two steps up  · the darkest text ever sits on */
  --color-rule:       oklch(34.5% 0.083 359);    /* #5A2438 · hairlines and frames */
  --color-rule-2:     oklch(77.2% 0.063 40);     /* #D9A897 · rules that carry text weight · 7.12:1 */
  --color-muted:      oklch(74.3% 0.061 38);     /* #CF9F90 · captions   ·  6.42:1 on paper-3 */
  --color-neutral:    oklch(81.0% 0.053 39);     /* #E0B6A8 · secondary  ·  8.14:1 */
  --color-ink-2:      oklch(90.7% 0.030 47);     /* #F2DBD0 · body       · 11.27:1 */
  --color-ink:        oklch(96.7% 0.019 55);     /* #FFF1E8 · primary    · 13.54:1 */

  /* ── The two lit hues, for surfaces with no words ──────────────
     Ember's gradient is built from these. They are FILLS: neither is a
     surface small text may sit on, and `check_contrast.py` records them as
     non-text with that reason rather than leaving them unrated.

     They replace --color-powder and --color-steel, which named the old
     palette's two supplied blues. The names go with the colours: a token
     called "powder" holding an orange is worse than a rename. */
  --color-flame:  oklch(70.5% 0.187 48);     /* #F97316 · the warm stop */
  --color-orchid: oklch(59.2% 0.218 1);      /* #DB2777 · the cool stop */

  /* One hue, four duties. On a dark ground the arithmetic inverts: the
     token a reader reads at body size has to be LIGHTER than the display
     one, not darker. `--color-accent-deep` keeps its name because
     check_design_tokens aliases report.css to it, and because "deep" has
     always meant "the accent at text weight" rather than "darker". Here
     that means lighter, and saying so is cheaper than a rename cascade. */
  --color-accent:      oklch(70.5% 0.187 48);     /* #F97316 · large display only · 5.34:1 */
  --color-accent-ink:  oklch(21.5% 0.052 347);    /* #2A0E1E · the plum, sitting ON an ember fill */
  --color-accent-deep: oklch(77.3% 0.148 57);     /* #FB9A4D · small labels, links, filled buttons · 7.01:1 */
  --color-focus:       oklch(77.3% 0.148 57);     /* #FB9A4D */

  /* Three outcomes, because the rubric has three and a report that renders
     "inconclusive" as a failure is lying. */
  --color-rated:        oklch(77.3% 0.148 57);     /* #FB9A4D */
  --color-inconclusive: oklch(75.8% 0.034 348);    /* #C2A8B4 */
  --color-refused:      oklch(71.6% 0.038 351);    /* #B79AA6 */

  /* ── Two the palette did not supply (D49) ─────────────────────
     A report has to say "this one is fine" and "this one needs attention",
     and Ember's two hues are both warm, so neither can carry either
     meaning without colliding with the accent.

     Derived rather than picked, as before: the hue sits clearly apart from
     the ember orange and the orchid, and the lightness is the value that
     clears 4.5:1 against paper-3. On a dark ground that means going up
     rather than down, so both are considerably lighter than the versions
     they replace. */
  --color-good:    oklch(84.2% 0.142 158);    /* #6EE7A8 · 9.73:1 on paper-3 */
  --color-caution: oklch(87.9% 0.153 92);     /* #FCD34D · 10.38:1 on paper-3 */

  /* ── Type · three families (D45a) ──────────────────────────────
     The site is concept C; the RENDERED REPORT keeps the broadsheet, and
     that split is the decision rather than a compromise. A marketing page
     is read by somebody deciding in eight seconds and has to look current.
     A report is read by somebody checking a number and has to look like a
     document that was signed.

     One correction to how D45a was framed: `templates/report.css` was never
     the broadsheet. It is a third design — green, mint, Inter — that
     predates the brand system entirely, so the paid deliverable a customer
     receives is off-brand today. Raised as D49 rather than swept into this
     change.

     Instrument Serif appears here in exactly one role — an italic word
     inside the display line — and nowhere else. It is a display cut with
     no weight range, so it is not a body face and is not asked to be. */
  --font-display: "Manrope", ui-sans-serif, system-ui, sans-serif;
  --font-body:    "Manrope", ui-sans-serif, system-ui, sans-serif;
  --font-serif:   "Instrument Serif", Georgia, "Times New Roman", serif;
  --font-label:   "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
  --font-mono:    "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;

  /* 800 was right for Big Shoulders, a condensed face that needs weight to
     hold a line. Instrument Sans at 800 reads as shouting; C sets 600. */
  --display-weight: 600;

  --text-xs:   0.75rem;
  --text-sm:   0.8125rem;
  --text-base: 1rem;
  --text-md:   1.0625rem;
  --text-lg:   1.25rem;
  --text-xl:   1.625rem;
  --text-2xl:  2.25rem;
  --text-3xl:  clamp(1.7rem, 3.4vw, 2.4rem);
  --text-4xl:  clamp(2.4rem, 5.6vw, 3.9rem);
  /* 11vw and 8rem were a condensed-face measurement: Big Shoulders is
     narrow enough to set a slogan at that size and still fit. Instrument
     Sans at 8rem puts two words on a line. */
  --text-display:   clamp(2.4rem, 5.6vw, 3.9rem);   /* the fold slogan */
  --text-display-s: clamp(1.9rem, 4vw, 2.8rem);
  --text-slab: clamp(1.8rem, 4vw, 2.6rem);     /* plank numerals */

  --lh-body:    1.6;
  --lh-snug:    1.3;
  --lh-tight:   1.05;
  /* Leading has to fall as type gets bigger and rise as it gets smaller,
     and the site had one value doing both jobs. 0.94 is a DISPLAY leading:
     right for a slogan set at 62px on two or three lines, wrong for a 20px
     subhead that wrapped to five. The homepage had exactly that -- five
     stage cards whose headings ran four and five lines at 0.94, so the
     descenders of one line touched the caps of the next.

     So --lh-display keeps the slogans, which set it explicitly, and
     --lh-heading is what an ordinary heading gets. 1.15 is the middle of
     the range a 20-40px heading wants; anything tighter starts colliding
     and anything looser stops reading as one heading. */
  --lh-heading: 1.15;
  --lh-display: 0.94;
  --tracking-display: -0.01em;
  --tracking-tight:   -0.01em;
  --tracking-label:    0.1em;
  --measure: 62ch;

  /* ── Space ─────────────────────────────────────────────────── */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs:  0.75rem;
  --space-sm:  1rem;
  --space-md:  1.5rem;
  --space-lg:  2.5rem;
  --space-xl:  4rem;
  --space-2xl: 6.5rem;
  --section-gap: 6.5rem;

  /* ── Press rules ───────────────────────────────────────────── */
  --rule-hair: 1px;
  --rule-fine: 2px;
  /* The broadsheet rounded nothing, on purpose: newsprint has no radii.
     C is a card system and every surface in it is rounded. */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 14px;

  /* ── Elevation (the operator console) ──────────────────────────
     The marketing pages are a document and use rules, not shadows. The
     review console is an application: twenty rows and four panels that
     have to separate from each other at a glance, and a hairline between
     everything reads as a wall, which is what it was.

     Tinted with the ink hue rather than black. A neutral-black shadow on
     a paper this warm goes grey and reads as dirt on the page. Two levels
     only -- resting and raised -- because a third is a decision nobody
     can make consistently at 11pm. */
  --shadow-1: 0 1px 2px oklch(32.8% 0.068 257 / 0.06),
              0 2px 8px oklch(32.8% 0.068 257 / 0.05);
  --shadow-2: 0 2px 4px oklch(32.8% 0.068 257 / 0.07),
              0 10px 28px oklch(32.8% 0.068 257 / 0.09);
  --radius-pill: 999px;

  --sheet-max: 1280px;
  --crop: clamp(0.75rem, 2.4vw, 2rem);   /* corner crop-mark inset */

  --ease-out:    cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in:     cubic-bezier(0.7, 0, 0.84, 0);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-snap:   cubic-bezier(0.2, 0.9, 0.2, 1);

  --dur-fast: 140ms;
  --dur-mid:  240ms;
  --dur-slow: 420ms;
}

/* ── The light theme ───────────────────────────────────────────
   D80b. Ember is the default and this is the alternative, reached only by
   the control in the header. It is NOT `prefers-color-scheme`: a reader
   whose OS is light still gets Ember until they say otherwise, because the
   dark ground is the brand rather than a preference.

   Same names, same duties, same twelve ratios measured against the same
   three surfaces. The only thing that moves is which end of the range each
   token sits at. `check_contrast.py` checks this block as its own theme,
   because a palette that passes in one mode and fails in the other is a
   palette that fails. */
:root[data-theme="light"] {
  --color-paper:      oklch(98.3% 0.010 58);     /* #FFF8F3 */
  --color-paper-2:    oklch(96.3% 0.018 54);     /* #FDF0E8 */
  --color-paper-3:    oklch(92.4% 0.029 51);     /* #F7E1D5 */
  --color-rule:       oklch(86.9% 0.041 51);     /* #EBCDBC */
  --color-rule-2:     oklch(45.8% 0.079 356);    /* #7A4459 · 5.96:1 on paper-3 */
  --color-muted:      oklch(45.8% 0.079 356);    /* #7A4459 · 5.96:1 */
  --color-neutral:    oklch(41.9% 0.074 351);    /* #6B3B52 · 7.03:1 */
  --color-ink-2:      oklch(31.4% 0.061 342);    /* #46243A · 10.60:1 */
  --color-ink:        oklch(21.5% 0.052 347);    /* #2A0E1E · 14.14:1 */

  --color-flame:  oklch(70.5% 0.187 48);     /* #F97316 */
  --color-orchid: oklch(59.2% 0.218 1);      /* #DB2777 */

  --color-accent:      oklch(55.3% 0.174 38);     /* #C2410C · large display only · 4.11:1 */
  --color-accent-ink:  oklch(98.3% 0.010 58);     /* #FFF8F3 */
  --color-accent-deep: oklch(47.0% 0.143 37);     /* #9A3412 · 5.81:1 */
  --color-focus:       oklch(47.0% 0.143 37);     /* #9A3412 */

  --color-rated:        oklch(47.0% 0.143 37);     /* #9A3412 */
  --color-inconclusive: oklch(41.6% 0.037 346);    /* #5B4450 */
  --color-refused:      oklch(45.0% 0.049 352);    /* #6B4A58 */

  --color-good:    oklch(44.8% 0.108 151);    /* #166534 · 5.67:1 on paper-3 */
  --color-caution: oklch(47.6% 0.103 62);     /* #854D0E · 5.44:1 */
}

*, *::before, *::after { box-sizing: border-box; }

html, body { margin: 0; overflow-x: clip; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
/* Clears the sticky masthead, at values taken from measuring it rather than
   from guessing: 210px at and below 375px, 170px to 767px, 98px above. The
   first attempt at this was 7rem everywhere, which is 112px, and left the
   target 58px under the header on a phone -- a fix that looks right in the
   file and does nothing on the device.
   Two stops, both at the site's existing 48rem, and both generous: a target
   that lands lower than it needs to is readable, and one that lands under
   the header is not. */
html { scroll-padding-top: 14rem; }
@media (min-width: 48rem) { html { scroll-padding-top: 7rem; } }

body {
  background: var(--color-paper);
  color: var(--color-ink-2);
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: var(--lh-body);
  font-optical-sizing: auto;
  /* Old-style figures suited Fraunces. Instrument Sans wants lining figures,
     and every number on this site sits in a row somebody compares down. */
  font-feature-settings: "kern", "liga";
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-style: normal;
  /* Was --lh-display (0.94), which is a slogan's leading applied to every
     heading on the site. The display lines set their own and keep it --
     .fold__slogan at 1.06, .hero h1 at 1.02 -- so this is the value an
     ordinary h2 or h3 gets, and those are the ones that wrap. */
  line-height: var(--lh-heading);
  /* Negative tracking is what makes a geometric sans read as set rather
     than typed. The broadsheet tracked positive because condensed caps
     need air; lowercase Instrument Sans needs the opposite. */
  letter-spacing: -0.021em;
  color: var(--color-ink);
  overflow-wrap: anywhere;
  min-width: 0;
  text-wrap: balance;
}

/* The one italic. Instrument Serif appears inside a display line and
   nowhere else on the site — an emphasis, not a second body face. It is
   the only italic here, because an italicised heading is otherwise the
   most reliable tell that nobody chose the type. */
h1 em, .fold__slogan em {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  color: var(--color-accent-deep);
}
p { margin: 0; max-width: var(--measure); }
a { color: inherit; text-decoration-color: var(--color-accent-deep); text-underline-offset: 3px; }
:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }

/* Form controls.
   These had no rules at all and were running on browser defaults, which is
   how the pricing and request pages overflowed by 18px at 320px: an input's
   intrinsic width does not shrink, and inside a flex or grid row
   `min-width: auto` stops it being squeezed. `max-width` and `min-width: 0`
   together are what actually let it fit.

   `font: inherit` matters as much: without it every input, textarea and
   select on the site renders in the browser's default face, so the one
   place a visitor types is the one place the typeface is wrong. */
input, textarea, select, button {
  font: inherit;
  max-width: 100%;
  min-width: 0;
}
textarea { width: 100%; }
/* The browser's default checkbox is 13x13. The label beside it toggles the
   control too, so the effective target clears the 24px floor either way --
   but 13px is a hard thing to hit on a phone whatever the spec says, and it
   is also visibly smaller than every other control on the page. */
input[type="checkbox"] {
  width: 1.15rem;
  height: 1.15rem;
  accent-color: var(--color-accent-deep);
}
.form__row, .scanbox__row { min-width: 0; }

.label {
  margin: 0;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}

/* ── Eyebrow ────────────────────────────────────────────────── */
/* C's pill above the fold line. It carries the scan's actual bounds --
   what it reads, how many pages, how long -- so the first thing on the
   page is a fact about the product rather than a promise about it. */
.eyebrow {
  display: inline-block;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  color: var(--color-muted);
  background: var(--color-paper-2);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-pill);
  padding: 0.35rem 0.85rem;
  margin-bottom: var(--space-md);
}

/* ── The sheet ──────────────────────────────────────────────── */
/* The crop marks are gone. They were a printer's mark on a page pretending
   to be printed; on a card layout they are decoration that means nothing.
   `.sheet--marked` is kept as a no-op so no page has to be edited for it. */

.sheet {
  position: relative;
  max-width: var(--sheet-max);
  margin: 0 auto;
  padding: 0 clamp(1.25rem, 5vw, 4rem);
}


/* ── Masthead ───────────────────────────────────────────────── */

/* C's bar: sticky, translucent, one hairline. The broadsheet's 3px rule
   under a 2xl uppercase wordmark was the loudest thing on the page; here
   the wordmark is the size of a nav item and the page below it is the
   thing being looked at. */
/* The sticky masthead needs the page to know how tall it is.
 *
 * Measured rather than assumed: with `position: sticky; top: 0` and no
 * scroll-padding anywhere in this file, `#scan` landed 63px under the header
 * and `#top` -- the skip link's own destination -- landed under all 170px of
 * it. So the accessibility escape hatch delivered you somewhere you could
 * not see. WCAG 2.4.11 Focus Not Obscured (Minimum), AA.
 *
 * The offset is declared here beside the thing it compensates for, so a
 * change to the header's height has one place to go. */
/* Full-bleed on purpose: this element paints the plate, the blur and the
   hairline, and a hairline that stops at --sheet-max leaves the gradient
   showing past both ends of the bar on any viewport wider than it. The
   width lives on `.masthead__inner` instead, which keeps `.nav`'s own box
   unchanged and with it the panel positioning below. */
.masthead {
  position: sticky;
  top: 0;
  z-index: 5;
  background:
    linear-gradient(rgba(0, 0, 0, 0.35), rgba(0, 0, 0, 0.35)),
    rgba(255, 255, 255, 0.06);
  backdrop-filter: blur(13px);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
@supports not (backdrop-filter: blur(10px)) {
  .masthead { background: var(--color-paper); }
}
.masthead__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
  padding: var(--space-xs) 0;
}
.masthead__mark {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-md);
  letter-spacing: -0.02em;
  text-decoration: none;
  color: var(--color-ink);
  white-space: nowrap;
}
.masthead__mark span { color: var(--color-accent-deep); }

/* The mark, inlined by scripts/build_site.inline_mark() from
   docs/brand/logo/mark.svg. Sized in `em` so it tracks the wordmark beside
   it rather than needing a second number every time the type scale moves,
   and it inherits `currentColor` — which is what lets one drawing serve the
   paper masthead and the tinted footer.

   1.4em, not 1em: the mark's brackets run the full height of its viewBox
   while the wordmark's cap height is about three quarters of its em, so
   matching the numbers makes the mark look half the size of the word next
   to it. This matches what the eye measures, which is the only thing that
   matters here.

   Inline rather than a flex item, and that is a fix rather than a
   preference. Making the link a flex container turned "Cite" and
   "<span>Unseen</span>" into two anonymous flex items, and the gap meant to
   sit between the mark and the word opened up inside the word as well:
   "Cite Unseen" in the one place the brand is written closed. */
.mark {
  height: 1.4em;
  width: auto;
  vertical-align: -0.28em;
  margin-right: 0.3em;
}
.masthead__issue { text-align: right; font-family: var(--font-label); font-size: var(--text-xs); color: var(--color-muted); }

/* The theme control.
   A pill in the masthead, sized to the 2.75rem target the rest of the site
   uses. The dot is the state: filled in Ember, hollow in light, so the
   control says which mode you are in without depending on the label alone. */
.themer {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  min-height: 2.75rem;
  min-width: 2.75rem;
  padding: 0;
  border: var(--rule-hair) solid var(--color-rule-2);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--color-ink);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  cursor: pointer;
}
/* `display` on a class beats the UA stylesheet's `[hidden] { display: none }`,
   so the button shipped with `hidden` was painted anyway and a reader with no
   script saw a theme control that could not do anything. The masthead comment
   states the opposite intent -- "a reader with no script sees the site in
   Ember and sees no control claiming to change it" -- and site.js clears the
   attribute (`themer.hidden = false`) to reveal it. This makes the attribute
   mean what it says. The site's other two `hidden` elements, #consent-open and
   .waiting, were checked with scripting off and were already correct. */
.themer[hidden] { display: none; }
.themer:hover { border-color: var(--color-accent-deep); }
/* Icon-only. The button is square rather than a pill with a word in it,
   and `.themer__label` stays in the markup as the accessible name — an
   icon button with no name is a button that announces itself as "button". */
.themer__icon {
  width: 1.15rem;
  height: 1.15rem;
  display: block;
  fill: none;
  stroke: var(--color-accent-deep);
  stroke-width: 1.7;
  stroke-linecap: round;
}
.themer__icon--sun > circle { fill: var(--color-accent-deep); stroke: none; }
.themer__icon--moon > path { fill: var(--color-accent-deep); stroke: none; }

/* Show the destination, not the current state: on Ember the control offers
   daylight, and in light it offers night back. */
.themer__icon--moon { display: none; }
:root[data-theme="light"] .themer__icon--sun { display: none; }
:root[data-theme="light"] .themer__icon--moon { display: block; }

.themer__label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.themer[aria-pressed="true"] .themer__dot { background: transparent; }

/* The issue line goes on a phone.
 *
 * Measured: the masthead was 237px of a 568px viewport at 320px, and 170px
 * at 390px, against 98px from 48rem up. It is sticky, so that is not a
 * one-time cost at the top of the page: it is a permanent bite out of every
 * screen a phone reader ever sees.
 *
 * "AI DISCOVERABILITY AUDIT" is the first thing the h1 underneath already
 * says, at greater length and with the actual argument. On a wide screen it
 * sits in otherwise empty space beside the mark and costs nothing. On a
 * phone it takes its own line and pushes the nav down one row. The cheapest
 * height to remove is the row that repeats the headline. */
@media (max-width: 39.99rem) {
  .masthead__issue { display: none; }
  .masthead__top { gap: var(--space-xs); }
}

/* ── The menu ────────────────────────────────────────────────
 *
 * Three groups and a price, not nine links in a row. The nine had drifted
 * into nine different lists across nine pages, and five pages had no nav at
 * all; this is one definition rendered everywhere (scripts/build_site.NAV).
 *
 * <details> and <summary> rather than script. That was forced when the CSP
 * said `script-src 'none'`; since D76 it is a choice, and still the right
 * one: this is the site's main navigation and it opens before any script
 * has run, for a reader with scripting off and for the crawlers this
 * product is about. It also gives keyboard and screen-reader support for
 * free, which a div-and-onclick menu does not.
 * ──────────────────────────────────────────────────────────── */
.nav {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  padding: 0 0 var(--space-xs);
  font-family: var(--font-display);
  font-size: var(--text-sm);
}
.nav__groups {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-md);
}
.nav__end {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

/* Static, so the panel below is positioned against `.nav` and spans it.
   Anchored to its own summary instead, three 24rem panels beside three
   labels 24px apart overlap each other, and the rightmost is clipped by the
   root `overflow-x: clip` with no scrollbar to reach it. */
.nav__group { position: static; }
.nav__flat,
.nav__account {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  gap: 0.35em;
  padding: 0.35rem 0;
  color: var(--color-neutral);
  text-decoration: none;
  cursor: pointer;
  /* A nav label must never wrap onto two lines: half of a wrapped label
     reads as a separate target and neither half looks clickable. */
  white-space: nowrap;
}
/* display stays list-item: WebKit exposes a <summary> as a disclosure
   control only while it is, and anything else demotes it to a generic
   element with no expanded/collapsed state announced. The flex layout goes
   on the inner span instead. */
.nav__group > summary {
  display: list-item;
  list-style: none;
  padding: 0.35rem 0;
  color: var(--color-neutral);
  cursor: pointer;
}
.nav__group > summary::-webkit-details-marker { display: none; }
.nav__label {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  white-space: nowrap;
}
/* The disclosure triangle, drawn rather than borrowed, so it can rotate. */
.nav__label::after {
  content: "";
  width: 0.4em;
  height: 0.4em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-0.15em) rotate(45deg);
  transition: transform 120ms ease;
}
.nav__group[open] .nav__label::after { transform: translateY(0.05em) rotate(-135deg); }

.nav__group > summary:hover,
.nav__flat:hover,
.nav__account:hover,
.nav__group[open] > summary { color: var(--color-ink); }

.nav__group > summary:focus-visible,
.nav__flat:focus-visible,
.nav__account:focus-visible,
.nav__cta:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 4px;
  border-radius: 3px;
}

.nav__panel {
  list-style: none;
  margin: 0;
  padding: var(--space-2xs);
  /* Anchored to `.nav`, so the panel spans the whole menu rather than
     hanging off its own label. Anchored to the label instead, three 24rem
     panels overlap each other and the rightmost is clipped by the root
     `overflow-x: clip` with no scrollbar to reach it.
     `<details name>` keeps only one open, so they never collide.

     In browsers older than late 2023 the `name` attribute is ignored and
     two panels can be open at once, landing in the same box: the later one
     paints over the earlier, whose links stay reachable by keyboard. That
     is a real degradation and it is accepted rather than missed — the
     alternative, laying the panel out in flow, costs every visitor a
     three-row menu with a content-width panel to spare a shrinking
     population a cosmetic overlap. Below 760px the panel IS in flow, where
     that trade does not apply. */
  position: absolute;
  top: calc(100% + 0.4rem);
  left: 0;
  right: 0;
  z-index: 20;
  display: grid;
  /* One row of columns across the full menu width, which is what makes this
     a mega menu rather than three dropdowns: four items fit side by side on
     a wide screen and collapse to one column when they no longer do. */
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.15rem;
  background: var(--color-paper-2);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: 12px;
  box-shadow: 0 12px 32px oklch(32.8% 0.068 257 / 0.12);
}
.nav__panel a {
  display: block;
  padding: 0.55rem 0.7rem;
  border-radius: 8px;
  text-decoration: none;
  color: var(--color-ink);
  /* Inside the panel wrapping is fine and expected: these are descriptions,
     not labels, and the whole block is one target. */
  white-space: normal;
}
.nav__panel a:hover { background: var(--color-paper-3); }
.nav__panel a[aria-current="page"] { background: var(--color-paper-3); }
.nav__flat[aria-current="page"] { color: var(--color-ink); font-weight: 600; }
.nav__panel a:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: -2px;
}
.nav__item-name { display: block; font-weight: 600; }
.nav__item-note {
  display: block;
  margin-top: 0.1rem;
  font-size: var(--text-xs);
  line-height: 1.45;
  color: var(--color-muted);
}

/* 44px, not 34. Measured at 320, 375 and 414: this was 122x34 in the
   masthead and 125x35 in the fold, and it is the primary action on every
   page. WCAG 2.5.8 asks 24x24 at AA and both platform guidelines ask 44;
   the site's own posture is that it is read on a phone at speed. */
.nav__cta {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: 0.4rem 0.95rem;
  border-radius: 999px;
  background: var(--color-ink);
  color: var(--color-paper);
  text-decoration: none;
  font-weight: 600;
  white-space: nowrap;
}
.nav__cta:hover { background: var(--color-ink-2); }

@media (prefers-reduced-motion: reduce) {
  .nav__label::after { transition: none; }
}

/* ── The fold ───────────────────────────────────────────────── */

/* C centres the fold. The broadsheet ran it off-axis with an indented
   second line, which is a newspaper move and reads as eccentric on a card
   page. `.fold__slogan .ln--indent` keeps its rule below so no page body
   has to change; it simply no longer indents. */
/* The fold is two columns from 62rem: the argument on the left, the one
   action on the right. Below that it stacks and the form comes second,
   which is the order somebody reads it in anyway. Centred text was fine
   for a slogan and wrong for a slogan plus a form plus three figures. */
/* ══ EMBER · the lit region ══════════════════════════════════
 *
 * D80c. The gradient lights the masthead and the fold and stops there.
 * Every page below the fold is flat plum, because the rubric and the
 * sample are long reading and a gradient under a thousand words of
 * findings is a gradient somebody has to read through.
 *
 * Painted on a `::before` fixed to the viewport and clipped to the region,
 * rather than on the element: a gradient on a scrolling box re-rasterises
 * every frame, and this one is 1400px tall.
 *
 * `pointer-events: none` and `aria-hidden` are not decoration. It sits
 * above the ground and below the content, and a decorative layer that eats
 * a click on the scan button is the kind of bug that looks like the form
 * is broken.
 */
/* Applied to `body`, so one declaration lights the masthead and whatever
   opens the page under it. The height is the band, not the page: 34rem
   covers the sticky masthead plus a hero on the home page, and the
   masthead plus a page title everywhere else. */
body { position: relative; isolation: isolate; }
body::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 46rem;
  z-index: -1;
  pointer-events: none;
  /* The band scrim, and it is load-bearing rather than atmospheric.
     The headline sits on the raw gradient with no plate under it, and
     Ember's cream ink over the full-strength flame stop measures 2.54:1 --
     under the 3:1 that large text requires. A 25% black veil over the
     whole band takes the worst stop to 4.27:1 and costs the gradient
     almost nothing you can see. `check_glass_contrast.py` reads this
     alpha out of this rule, so lightening it fails the build. */
  background:
    linear-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.25)),
    radial-gradient(120% 78% at 8% 0%,    var(--color-flame)  0%, transparent 52%),
    radial-gradient(110% 72% at 96% 22%,  var(--color-orchid) 0%, transparent 52%),
    radial-gradient(130% 86% at 44% 104%, var(--color-paper-3) 0%, transparent 58%);
}
/* The light stops at the fold rather than ending at it. A hard edge
   between a lit region and a flat one reads as a rendering fault; 140px of
   fade reads as the light running out. */
body::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: calc(46rem - 160px);
  height: 160px;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--color-paper));
}

/* ── The frosted plates ──────────────────────────────────────
 *
 * The scrim is the load-bearing part and it is there for a measured
 * reason. Glassmorphism normally ships a plate of white at 10-12%. Over
 * Ember's brightest stop that composites to #FA8432, and the near-white
 * text on it measures 2.26:1 — a failure, not a close call.
 *
 * 6% white over a 35% black scrim composites to #A8561C instead, and the
 * same text measures 4.73:1. It is the same frosted look and it passes.
 * The two layers are one `background` shorthand so nothing can apply the
 * plate without the scrim under it.
 *
 * `check_glass_contrast.py` holds this: it composites the declared plate
 * over the brightest stop of the declared gradient and fails below 4.5.
 */
.plate {
  padding: var(--space-sm) var(--space-md);
  background:
    linear-gradient(rgba(0, 0, 0, 0.35), rgba(0, 0, 0, 0.35)),
    rgba(255, 255, 255, 0.06);
  border: var(--rule-hair) solid rgba(255, 255, 255, 0.22);
  border-radius: var(--radius-md);
  backdrop-filter: blur(13px);
  -webkit-backdrop-filter: blur(13px);
}
/* A browser with no backdrop-filter gets the same rectangle with a solid
   ground instead of a blurred one. The alternative is a transparent plate
   over a gradient, which is the unreadable case this whole comment is
   about. */
/* The light theme inverts the scrim with the ground.
   A 35% BLACK scrim under light-theme ink is a dark plate carrying dark
   text, which is the same failure as no scrim at all, in the other
   direction. Light mode puts white over the gradient instead, and
   check_glass_contrast measures this block against the light ink exactly
   as it measures the block above against Ember's. */
:root[data-theme="light"] body::before {
  background:
    linear-gradient(rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.15)),
    radial-gradient(120% 78% at 8% 0%,    var(--color-flame)  0%, transparent 52%),
    radial-gradient(110% 72% at 96% 22%,  var(--color-orchid) 0%, transparent 52%),
    radial-gradient(130% 86% at 44% 104%, var(--color-paper-3) 0%, transparent 58%);
}
:root[data-theme="light"] .plate,
:root[data-theme="light"] .masthead {
  background:
    linear-gradient(rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0.62)),
    rgba(255, 255, 255, 0.06);
  border-color: rgba(0, 0, 0, 0.10);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .plate { background: var(--color-paper-3); }
}

.fold {
  padding: clamp(var(--space-lg), 7vw, var(--space-xl)) 0 var(--space-lg);
  display: grid;
  gap: var(--space-lg);
  align-items: start;
  text-align: left;
}
@media (min-width: 62rem) {
  /* Two columns, three children. The readout was lifted out of
     `.fold__arg` so a phone can put the form above it, which made it a
     third grid item and pushed the form into a row of its own -- the scan
     box went from 219px down the page to 714px on a 1024px screen. Named
     areas put it back under the argument where it belongs. */
  .fold {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
    grid-template-areas:
      "arg  act"
      "read act"
      "demo demo";
    align-content: start;
    gap: var(--space-md) var(--space-xl);
  }
  .fold__arg     { grid-area: arg; }
  .fold__act     { grid-area: act; }
  .fold .readout { grid-area: read; }
}
.fold__arg, .fold__act { min-width: 0; }
.fold__act .scanbox { width: 100%; }
.fold__slogan { font-size: var(--text-display); max-width: 17ch; margin: 0; line-height: 1.04; }
/* The broadsheet set the slogan as three hand-broken lines, because a
   newspaper deck is composed line by line. C lets it wrap at its measure,
   so `.ln` no longer forces a break — a hand-set break at 17ch becomes a
   ragged accident at any other width. Kept as inline so a page that still
   carries the span is unaffected. */
.fold__slogan .ln { display: inline; }
/* Was: a whole line in red. Two red lines out of three is not an accent,
   it is a second body colour — and the supplied red is a DISPLAY red that
   fails AA at small sizes, so a line of it is also a hazard the moment the
   viewport narrows. C carries the emphasis on one italic word instead. */
.fold__slogan .ln--red { color: inherit; }
.fold__slogan .ln--indent { padding-left: 0; }

.fold__lede {
  margin: var(--space-sm) 0 0;
  max-width: 46ch;
  font-size: var(--text-md);
  color: var(--color-neutral);
}
.fold__actions {
  display: flex;
  gap: var(--space-md);
  align-items: center;
  flex-wrap: wrap;
  margin-top: var(--space-lg);
}

.btn {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  text-decoration: none;
  padding: 0.72rem 1.25rem;
  border-radius: var(--radius-sm);
  border: var(--rule-hair) solid var(--color-ink);
  background: var(--color-ink);
  color: var(--color-paper);
  transition: background 120ms ease, transform 60ms ease;
}
/* D73a: the accent marks the primary action, once per view. Every button
   carried it, which is the same as no button carrying it. Buttons are ink
   now; .btn--go — the scan, the one action this site exists for — is the
   exception and the only accent fill on the page. */
.btn--go {
  border-color: var(--color-accent-deep);
  background: var(--color-accent-deep);
  color: var(--color-accent-ink);
}
.btn--go:hover, .btn--go.is-hover {
  background: var(--color-accent);
  border-color: var(--color-accent);
}
@media (prefers-reduced-motion: reduce) { .btn { transition: none; } }
.btn--ghost {
  background: transparent;
  color: var(--color-ink);
  border-color: var(--color-rule);
}
.btn--ghost:hover, .btn--ghost.is-hover {
  background: var(--color-paper-2);
  border-color: var(--color-ink);
}
.btn--lg { padding: 0.9rem 1.6rem; font-size: var(--text-md); }
.btn:hover, .btn.is-hover { background: var(--color-ink-2); border-color: var(--color-ink-2); }
.btn:active, .btn.is-active { transform: translateY(1px); }

.link-arrow {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  color: var(--color-ink-2);
  border-bottom: var(--rule-hair) solid var(--color-rule);
  padding-bottom: 2px;
}
.link-arrow:hover { border-bottom-color: var(--color-ink); color: var(--color-ink); }
/* 24px is the floor in WCAG 2.2 AA (2.5.8 Target Size (Minimum)) and this
   was 22 -- a label-sized link is short by exactly the amount a label is
   smaller than body text. The height is added below the baseline rather
   than around it so the underline keeps sitting on the words. */
.link-arrow {
  display: inline-flex;
  align-items: flex-start;
  min-height: 1.5rem;
}

/* ── Planks · the five stages, as cards ─────────────────────── */
/* Was a hand-numbered broken grid: each plank at a different indent so the
   column never settled. That was the broadsheet's signature and it is the
   first thing C drops — an off-grid stack asks the reader to work out the
   order, and the order is the whole content here. Same markup, now an
   even card grid that reads left-to-right without instruction. */

.spine { padding-top: var(--space-xl); border-top: var(--rule-hair) solid var(--color-rule); }
.spine__title { font-size: var(--text-3xl); max-width: 20ch; }
.spine__note { margin-top: var(--space-sm); color: var(--color-neutral); font-size: var(--text-base); max-width: 58ch; }

/* Five stages, as one ruled object rather than five cards. A card says
   "separate thing"; these are five steps of one process, and a shared frame
   with hairline dividers says that instead. One column below 48rem, two to
   62rem, five above it. */
.planks {
  margin-top: var(--space-lg);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
  overflow: hidden;
}
@media (min-width: 48rem) { .planks { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 62rem) { .planks { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.plank {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2xs);
  align-content: start;
  padding: var(--space-md) var(--space-sm);
  border-bottom: var(--rule-hair) solid var(--color-rule);
  background: var(--color-paper);
}
@media (min-width: 48rem) {
  .plank { border-right: var(--rule-hair) solid var(--color-rule); }
  .plank:nth-child(2n) { border-right: none; }
}
@media (min-width: 62rem) {
  .plank { border-bottom: none; border-right: var(--rule-hair) solid var(--color-rule); }
  .plank:nth-child(2n) { border-right: var(--rule-hair) solid var(--color-rule); }
  .plank:last-child { border-right: none; }
}
/* The indent modifiers stay declared and do nothing, so no page body has
   to be edited to remove a grid that no longer exists. */
.plank--hang, .plank--drop, .plank--wide { margin-left: 0; }

.plank__no {
  font-family: var(--font-label);
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  line-height: 1;
  color: var(--color-ink-2);
  font-variant-numeric: tabular-nums;
}
.plank__body { min-width: 0; }
.plank h3 { font-size: var(--text-md); margin-bottom: var(--space-2xs); }
.plank h3 small {
  display: block;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  font-weight: 400;
  text-transform: uppercase;
  color: var(--color-muted);
  margin-bottom: var(--space-3xs);
}
.plank p { font-size: var(--text-base); color: var(--color-neutral); }
.plank p + p { margin-top: var(--space-sm); }
.plank__aside {
  margin-top: var(--space-sm);
  padding-left: var(--space-sm);
  border-left: 3px solid var(--color-rule);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* ── The notice · what this will not tell you ───────────────── */

.notice { padding-top: var(--space-xl); border-top: var(--rule-hair) solid var(--color-rule); }
.notice h2 { font-size: var(--text-3xl); max-width: 22ch; }
.notice ul { list-style: none; margin: var(--space-lg) 0 0; padding: 0; }
.notice li {
  display: grid;
  grid-template-columns: 2.5ch minmax(0, 1fr);
  gap: var(--space-sm);
  padding: var(--space-md) 0;
  border-bottom: var(--rule-hair) solid var(--color-rule);
  max-width: var(--measure);
  color: var(--color-ink-2);
}
.notice li::before {
  content: "\2014";
  font-family: var(--font-label);
  color: var(--color-refused);
}

/* ── Pricing ────────────────────────────────────────────────── */

.rates { padding-top: var(--space-xl); border-top: var(--rule-hair) solid var(--color-rule); }
/* `:not(.label)` because /pricing/ writes `<h2 class="label">`, and this
   rule was inflating an eyebrow — mono, uppercase, letterspaced, muted — to
   38px. The index's .rates h2 is an ordinary display heading and still is. */
.rates h2:not(.label) { font-size: var(--text-3xl); }
/* Four tiers, one ruled object. Same reasoning as the stages: these are
   four points on one scale, not four unrelated things, and a shared frame
   says so. --hi keeps its emphasis as a fill rather than a second border,
   because a border inside a bordered grid reads as a rendering fault. */
.tiers {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-top: var(--space-lg);
}
@media (min-width: 48rem) { .tiers { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 62rem) { .tiers { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.tier {
  background: var(--color-paper);
  padding: var(--space-md) var(--space-sm);
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
@media (min-width: 48rem) {
  .tier { border-right: var(--rule-hair) solid var(--color-rule); }
  .tier:nth-child(2n) { border-right: none; }
}
@media (min-width: 62rem) {
  .tier { border-bottom: none; border-right: var(--rule-hair) solid var(--color-rule); }
  .tier:nth-child(2n) { border-right: var(--rule-hair) solid var(--color-rule); }
  .tier:last-child { border-right: none; }
}
.tier--hi { background: var(--color-paper-2); }
.tier .btn { margin-top: auto; align-self: start; }
.tier h3 { font-size: var(--text-md); }
.tier__price {
  display: block;
  margin: var(--space-2xs) 0;
  font-family: var(--font-label);
  font-weight: 600;
  font-size: var(--text-xl);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.tier__per {
  display: block;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.tier p { font-size: var(--text-base); color: var(--color-neutral); }

/* ── The pledge · the sample, stated against ourselves ──────── */

/* This was already C's proof bar in all but shape: a dark ink block whose
   only content is figures about our own work. It just needed the corners. */
.pledge {
  margin-top: var(--space-2xl);
  padding: var(--space-xl) clamp(1.25rem, 4vw, var(--space-xl));
  background: var(--color-ink);
  color: var(--color-paper);
  border-radius: var(--radius-lg);
}
.pledge h2 { color: var(--color-paper); font-size: var(--text-3xl); max-width: 20ch; }
.pledge p { color: var(--color-paper); opacity: 0.82; margin-top: var(--space-md); max-width: 58ch; }
.pledge__figures {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-md);
  margin-top: var(--space-lg);
  padding-top: var(--space-lg);
  border-top: var(--rule-hair) solid var(--color-muted);
}
@media (min-width: 40rem) { .pledge__figures { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 62rem) { .pledge__figures { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.pledge__figures div { min-width: 0; }
.pledge dt {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-paper);
  opacity: 0.7;
}
.pledge dd {
  margin: var(--space-3xs) 0 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-2xl);
  font-variant-numeric: tabular-nums;
  color: var(--color-paper);
  overflow-wrap: anywhere;
}
.pledge dd small {
  display: block;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--text-sm);
  text-transform: none;
  letter-spacing: 0;
  opacity: 0.7;
}
.pledge .btn { margin-top: var(--space-lg); }


/* ── Colophon ───────────────────────────────────────────────── */

.colophon {
  margin-top: var(--space-2xl);
  padding: var(--space-md) 0 var(--space-2xl);
  border-top: var(--rule-hair) solid var(--color-rule);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  line-height: 1.8;
  color: var(--color-muted);
}
.colophon p { max-width: 82ch; }
.colophon p + p { margin-top: var(--space-sm); }
.colophon b { color: var(--color-ink); font-weight: 500; }
.colophon a { color: var(--color-ink-2); white-space: nowrap; }

/* ── Sample pages ───────────────────────────────────────────────
 *
 * /sample/ and /sample-tln/ were authored against the modern-minimal
 * stylesheet that D22 did not choose, and were left referencing classes this
 * file has never defined — .rail, .hero, .wrap, .page, .artifacts,
 * .refusals, .facts. They have been serving essentially unstyled since the
 * broadsheet was promoted to the index: an unstyled document is a poor
 * advertisement for a product whose subject is how a machine reads a page.
 *
 * These map that markup onto the broadsheet rather than rewriting the pages,
 * so the two samples and the index are demonstrably one site.
 */

.page { padding-bottom: var(--space-2xl); }
/* A prose page that is not wrapped in .page still needs air above the
   footer; without this its last line sat on the footer's top rule. */
main > .wrap.prose:last-child { padding-bottom: var(--space-2xl); }
.wrap {
  max-width: var(--sheet-max);
  margin: 0 auto;
  padding: 0 clamp(1.25rem, 5vw, 4rem);
}

/* The side rail is the masthead's nav in a different arrangement, so it is
   styled as one — same face, same rules, same hover. */
.rail {
  max-width: var(--sheet-max);
  margin: 0 auto;
  padding: var(--space-xl) clamp(1.25rem, 5vw, 4rem) 0;
}
.rail__mark {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-2xl);
  text-transform: uppercase;
  letter-spacing: var(--tracking-display);
  color: var(--color-ink);
  text-decoration: none;
  padding-bottom: var(--space-xs);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
.rail__mark span { color: var(--color-accent); }
.rail__nav {
  display: flex;
  gap: clamp(var(--space-sm), 3vw, var(--space-lg));
  padding: var(--space-2xs) 0 var(--space-sm);
  border-bottom: var(--rule-hair) solid var(--color-rule);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  /* One scrolling row rather than wrapped rows, for the same reason the
     index's nav is: on a small screen wrapped nav eats the first screen. */
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}
.rail__nav::-webkit-scrollbar { display: none; }
.rail__nav a { text-decoration: none; white-space: nowrap; color: var(--color-ink); }
.rail__nav a:hover { color: var(--color-accent-deep); text-decoration: underline; }
.rail__foot {
  padding-top: var(--space-2xs);
  color: var(--color-muted);
}

.hero { padding: clamp(var(--space-md), 3vw, var(--space-lg)) 0 var(--space-lg); }
.hero__stages {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
  margin-bottom: var(--space-2xs);
}
.hero__stages b { color: var(--color-ink); font-weight: 600; }
.hero h1 {
  font-size: clamp(1.9rem, 4.6vw, 3.4rem);
  /* 1.06, the same as `.fold__slogan`. It was 1.02, which is two display
     leadings on one site for no reason anybody could name, and on the three
     document pages whose h1 wraps to three lines it put the descenders of
     one line into the caps of the next. */
  line-height: 1.06;
  max-width: 22ch;
  text-wrap: balance;
  overflow-wrap: anywhere;
  min-width: 0;
}
.hero__lede {
  font-size: var(--text-lg);
  max-width: var(--measure);
  margin-top: var(--space-sm);
}
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  margin-top: var(--space-lg);
}

.refusals { padding-top: var(--space-xl); border-top: var(--rule-hair) solid var(--color-rule); }
.refusals h2 { font-size: var(--text-2xl); max-width: 24ch; margin-bottom: var(--space-md); }
.refusals ul { margin: 0; padding-left: 1.1rem; max-width: var(--measure); }
.refusals li { margin-bottom: var(--space-sm); color: var(--color-ink-2); }

/* ── Questions · a broadsheet Q&A column ────────────────────── */

.qa { padding-top: var(--space-xl); border-top: var(--rule-hair) solid var(--color-rule); }
.qa h2 { font-size: var(--text-3xl); max-width: 20ch; margin-bottom: var(--space-lg); }
.qa details { border-bottom: var(--rule-hair) solid var(--color-rule); max-width: var(--measure); }
.qa summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  gap: var(--space-sm);
  align-items: baseline;
  padding: var(--space-md) 0;
}
.qa summary::-webkit-details-marker { display: none; }
.qa summary::before {
  content: "+";
  font-family: var(--font-label);
  color: var(--color-ink-2);
  flex: none;
}
.qa details[open] summary::before { content: "\2212"; }
.qa summary h3 { font-size: var(--text-md); margin: 0; }
.qa__a { padding: 0 0 var(--space-md) calc(var(--space-sm) + 1ch); }
.qa__a p { font-size: var(--text-base); color: var(--color-ink-2); }

/* ── The scan box · the offer, above the fold ───────────────── */

.scanbox {
  /* Centred, because the fold above it is. A left-aligned box under a
     centred headline reads as two decisions rather than one. */
  margin: var(--space-lg) auto 0;
  padding: var(--space-md);
  background: var(--color-paper-2);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
  max-width: 40rem;
  text-align: left;
}
.scanbox__label {
  display: block;
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: var(--text-xl);
  line-height: var(--lh-tight);
  color: var(--color-ink);
  margin-bottom: var(--space-xs);
}
.scanbox__row { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
.scanbox__input {
  flex: 1 1 18rem;
  min-width: 0;                     /* or a long URL pushes the button off */
  padding: 0.75rem var(--space-sm);
  font-family: var(--font-label);
  font-size: var(--text-base);      /* 16px: anything smaller zooms on iOS */
  color: var(--color-ink);
  background: var(--color-paper);
  border: var(--rule-hair) solid var(--color-rule-2);
  border-radius: var(--radius-sm);
}
.scanbox__input::placeholder { color: var(--color-muted); }
.scanbox__input:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: 1px;
}
.btn--go { flex: 0 0 auto; border: none; cursor: pointer; font: inherit; }
.scanbox__note {
  margin: var(--space-xs) 0 0;
  font-size: var(--text-sm);
  color: var(--color-ink-2);
  max-width: var(--measure);
}
/* The optional address. Set apart from the URL row rather than beside it:
   the scan needs one field and the email is a second, smaller request, and
   a form that presents them as equals reads as two required fields. */
.scanbox__opt {
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: var(--rule-hair) solid var(--color-rule);
}
.scanbox__optlabel {
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
  font-family: var(--font-label);
  font-size: var(--text-sm);
  color: var(--color-ink);
  margin-bottom: var(--space-2xs);
}
.scanbox__optional {
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-muted);
}
.scanbox__input--email { flex: 1 1 100%; width: 100%; }
.scanbox__opt .scanbox__note { margin-top: var(--space-2xs); }

/* The email field and its consent copy were ~90 words sitting between the
   URL box and the decision to use it. They are the same words, behind a
   disclosure: somebody who wants the link in their inbox opens it, and
   everybody else sees a one-line form. No script — <details> is the
   element for exactly this. */
.scanbox__opt { margin-top: var(--space-xs); }
.scanbox__opt > summary {
  display: list-item;                 /* keep the marker WebKit exposes */
  cursor: pointer;
  min-height: 2.75rem;                /* 44px: the touch floor, D-sweep 8 */
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  font-family: var(--font-label);
  font-size: var(--text-sm);
  color: var(--color-ink-2);
}
.scanbox__opt > summary:hover { color: var(--color-ink); }
.scanbox__opt[open] > summary { margin-bottom: var(--space-2xs); }

/* Staged for D72's scripted build. Nothing sets data-state today, so this
   rule cannot fire; it is kept rather than deleted because the scan form is
   the first place a busy state will be needed. */
.scanbox[data-state="busy"] .btn--go { opacity: 0.6; pointer-events: none; }
.scanbox__err {
  margin: var(--space-xs) 0 0;
  font-family: var(--font-label);
  font-size: var(--text-sm);
  color: var(--color-accent-deep);
}

/* ── The nudge · what the scan does not cover ───────────────── */

.nudge {
  margin: var(--space-xl) 0;
  padding-top: var(--space-md);
  border-top: var(--rule-hair) solid var(--color-rule);
}
.nudge__h {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: var(--text-2xl);
  line-height: var(--lh-tight);
  color: var(--color-ink);
  margin: 0 0 var(--space-md);
  text-wrap: balance;
}
/* The next step, in a sentence. This block used to carry a second price
   list and the page therefore stated its prices twice in two different
   shapes. */
.nudge__p {
  margin: var(--space-sm) 0 0;
  max-width: var(--measure);
  font-size: var(--text-base);
  color: var(--color-ink-2);
}
.nudge__p .nudge__price { font-variant-numeric: tabular-nums; }
.nudge__list {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--space-md);
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}
.nudge__list li {
  min-width: 0;
  display: flex; flex-direction: column; gap: var(--space-3xs);
  border-top: var(--rule-fine) solid var(--color-ink);
  padding-top: var(--space-2xs);
}
.nudge__price {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: var(--text-2xl);
  font-variant-numeric: tabular-nums;
  color: var(--color-ink);
  line-height: 1;
}
.nudge__price i {
  font-family: var(--font-label); font-style: normal;
  font-size: var(--text-xs); letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--color-muted);
}
.nudge__list b { font-size: var(--text-md); color: var(--color-ink); }
.nudge__list span:not(.nudge__price) { font-size: var(--text-sm); color: var(--color-ink-2); }
.nudge__list .link-arrow { margin-top: auto; align-self: start; font-size: var(--text-sm); }

/* The offer has to be reachable without scrolling on a small phone too, so
   the masthead and the fold give up their generosity before the scan box
   gives up its place. */
@media (max-width: 47.99rem) {
  /* Below this the panel opens in flow beneath its own label rather than
     floating over the page: on a phone a floating panel is a modal nobody
     asked for, and it covers the offer.

     A closed group stays inline in the label row; an OPEN one takes a full
     row, so its panel sits directly under it and two open panels stack
     instead of overlapping. That matters because `<details name>` — the
     exclusive-accordion attribute that keeps only one open — is ignored by
     browsers older than late 2023, and two panels sharing one anchor land
     in the same box. `width: 100%` alone does not do it: a flex item still
     shrinks to share a line, so the two sat side by side at 48px and 55px
     wide and overlapped anyway. `flex: 0 0 100%` refuses to shrink, which
     is what forces the break. */
  .nav { gap: var(--space-2xs); }
  .nav__groups { gap: var(--space-2xs) var(--space-sm); }
  /* A flex item still shrinks to share a line, so `width: 100%` alone left
     two open groups side by side at 48px and 55px wide with their in-flow
     panels overlapping anyway. `flex: 0 0 100%` refuses to shrink, which is
     what forces the break that gives each panel its own row. */
  .nav__group[open] { flex: 0 0 100%; }
  .nav__panel {
    position: static;
    grid-template-columns: minmax(0, 1fr);
    box-shadow: none;
  }
  .nav__end { width: 100%; justify-content: space-between; }
  .masthead { padding-top: var(--space-md); padding-bottom: var(--space-2xs); }
  .masthead__top { gap: var(--space-2xs); }
  .fold { padding-top: var(--space-sm); padding-bottom: var(--space-md); }
  .fold__lede { font-size: var(--text-base); margin-top: var(--space-2xs); }
  .scanbox { padding: var(--space-sm); }
  .scanbox__label { font-size: var(--text-lg); }
  .scanbox__note { font-size: var(--text-xs); }
}

/* A published correction. It sits directly under the hero on the sample page
   because a footnote below a wrong instruction is not a correction — the
   reader has already acted on the instruction by the time they reach it. */
.correction {
  margin-top: var(--space-lg);
  padding: var(--space-md) var(--space-md) calc(var(--space-md) - 0.4rem);
  background: var(--color-paper-2);
  border-left: 3px solid var(--color-inconclusive);
}

/* A second correction in the same box needs air above it; the first one is
   already spaced by the box's own padding. `correction__h2` — a class
   nothing in this file defined — was doing that job on /sample/, which is
   why the later heading sat 4px off its own paragraph. */
* + .correction__h { margin-top: var(--space-md); }
.correction__h {
  margin: 0 0 var(--space-sm);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-inconclusive);
}

.correction p,
.correction li {
  margin: 0 0 var(--space-sm);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--color-ink-2);
  max-width: 68ch;
}

.correction ul {
  margin: 0 0 var(--space-sm);
  padding-left: 1.2em;
}

.correction li {
  margin-bottom: 0.35rem;
}

/* ---------------------------------------------------------------------- *
 * Site footer
 *
 * One footer on every page, rendered from the shell. It exists because our
 * own TA-04 check found pages with no inbound internal link at all — the new
 * pages were reachable only from whichever page happened to link them.
 *
 * Five columns on a wide screen, one on a phone. The brand column is wider
 * than the link columns because it carries a sentence rather than a list,
 * and a sentence set to the same width as a list of four words reads as a
 * mistake.
 * ---------------------------------------------------------------------- */
/* The skip link.
 *
 * It had NO rule at all. Not lost in the refresh: twenty-five commits of
 * history say it never had one, so "Skip to content" rendered as ordinary
 * body text above the masthead on all 22 pages, from the day the shell was
 * written. `templates/report.css` styles it correctly, which is most of why
 * nobody caught it here: the delivered artifact looked right.
 *
 * The pattern is the same one `.visually-hidden` uses, plus a visible state
 * on focus, because a skip link that stays hidden when focused is useless to
 * the keyboard user it exists for. WCAG 2.4.1 Bypass Blocks. */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 10;
  transform: translateY(-120%);
  padding: var(--space-2xs) var(--space-sm);
  background: var(--color-ink);
  color: var(--color-paper);
  text-decoration: none;
  border-radius: 0 0 var(--radius-sm) 0;
}
.skip-link:focus {
  transform: translateY(0);
}

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

.sitefoot {
  border-top: var(--rule-hair) solid var(--color-rule);
  background: var(--color-paper-2);
  padding: var(--space-xl) clamp(1.25rem, 5vw, 4rem) var(--space-lg);
}
.sitefoot__inner {
  max-width: var(--sheet-max);
  margin: 0 auto;
  display: grid;
  /* Two blocks, not five. The four link columns became one evidence index
     and an inline colophon run below; the old `1.6fr + repeat(4, 1fr)`
     left three empty tracks and the footer read as a page with the middle
     deleted. The index is given room to sit as two readable columns rather
     than one tall list. */
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: var(--space-lg) var(--space-xl);
}
.sitefoot__brand { max-width: 30rem; }
.sitefoot__mark {
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-lg);
  letter-spacing: -0.02em;
  text-decoration: none;
  color: var(--color-ink);
}
.sitefoot__mark span { color: var(--color-accent-deep); }
.sitefoot__blurb {
  margin: var(--space-2xs) 0 var(--space-sm);
  color: var(--color-muted);
  font-size: var(--text-sm);
  line-height: 1.6;
}
.sitefoot__cta {
  display: inline-flex;
  align-items: center;
  padding: 0.45rem 0.95rem;
  border-radius: 999px;
  background: var(--color-ink);
  color: var(--color-paper);
  text-decoration: none;
  font-weight: 600;
  font-size: var(--text-sm);
  white-space: nowrap;
}
.sitefoot__cta:hover { background: var(--color-ink-2); }
/* The last two under-sized targets on the page, measured at 320, 375 and
   414: the footer's own call to action at 125x35 and the address in the
   copyright line at 113x17. Everything narrower than 44px that remains is a
   text link in a wrapped row, which clears the 24x24 that WCAG 2.5.8 asks
   for at AA. */
.sitefoot__cta { min-height: 2.75rem; }
.sitefoot__copy a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
}

.sitefoot__h {
  margin: 0 0 var(--space-2xs);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-muted);
}
.sitefoot__col ul {
  list-style: none; margin: 0; padding: 0;
  columns: 2; column-gap: var(--space-lg);
}
@media (max-width: 47.99rem) { .sitefoot__col ul { columns: 1; } }
.sitefoot__col li + li { margin-top: 0.1rem; }
/* The hit area is the row, not the words. These were about 32px tall and
   there are a lot of them, which on a phone is a column of near-misses. */
.sitefoot__col a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  color: var(--color-ink);
  text-decoration: none;
  font-size: var(--text-sm);
  /* A footer link must not wrap onto two lines: half of a wrapped link
     reads as a separate target and neither half looks clickable. */
  white-space: nowrap;
}
.sitefoot__col a:hover { text-decoration: underline; }

.sitefoot__col a:focus-visible,
.sitefoot__mark:focus-visible,
.sitefoot__cta:focus-visible,
.sitefoot__base a:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 3px;
  border-radius: 3px;
}

/* The colophon run. One wrapped line of destinations, replacing three of
   the four columns this footer used to carry. */
.sitefoot__run {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--space-md);
  margin: 0 0 var(--space-sm);
  font-size: var(--text-sm);
}
.sitefoot__run a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  color: var(--color-neutral);
  text-decoration: none;
}
.sitefoot__run a:hover { color: var(--color-ink); text-decoration: underline; }
.sitefoot__run a:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 3px;
}

.sitefoot__base {
  max-width: var(--sheet-max);
  margin: var(--space-lg) auto 0;
  padding-top: var(--space-sm);
  border-top: var(--rule-hair) solid var(--color-rule);
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2xs) var(--space-md);
}
.sitefoot__legal,
.sitefoot__copy {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-xs);
  line-height: 1.6;
}
/* 44rem of 12px text is about 105 characters a line. Fine print is still
   print: the eye loses the return sweep past 90 in exactly the paragraph a
   reader is least motivated to finish. */
.sitefoot__legal { max-width: 68ch; }
.sitefoot__copy a { color: inherit; }

@media (max-width: 61.99rem) {
  .sitefoot__inner { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sitefoot__brand { max-width: none; }
  .sitefoot__brand { grid-column: 1 / -1; }
}
@media (max-width: 39.99rem) {
  .sitefoot { padding-top: var(--space-lg); }
  .sitefoot__inner { grid-template-columns: minmax(0, 1fr); gap: var(--space-md); }
  .sitefoot__base { margin-top: var(--space-md); }
}

/* Wide content scrolls inside its own container so the page body never
   scrolls sideways. The pricing table is 388px of content in a 320px
   viewport, and it had been pushing the whole document 88px wide on a small
   phone — found by driving a real browser at 320/375/414/768/1280 rather
   than by reading the CSS. Lighthouse's desktop preset cannot see it. */
.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.table-wrap > table { min-width: 24rem; }

/* ── The fact strip ─────────────────────────────────────────────
 *
 * The number band under a hero: score, grade, coverage, what moved.
 *
 * It lived in `site/_pages/sample/page.css` and again, reformatted, in
 * `site/_pages/sample-tln/page.css`, and /remediation/ — which has no page
 * stylesheet — used the same markup and got none of it. So the page whose
 * whole subject is a before-and-after rendered its four headline numbers as
 * an unstyled `<dl>`: the label on one line, the number indented under it,
 * and the caption running into the number with no space.
 *
 * A component used by three pages and defined by two of them is the same
 * defect as a colour typed twice. One definition, here, and the page
 * stylesheets keep only what is genuinely theirs. */
.facts {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-md);
  padding: var(--space-lg) 0;
  border-top: var(--rule-hair) solid var(--color-rule-2);
  border-bottom: var(--rule-hair) solid var(--color-rule);
  margin-top: var(--space-lg);
}
.facts div { min-width: 0; }
.facts dt {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.facts dd {
  margin: var(--space-3xs) 0 0;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-lg);
  color: var(--color-ink);
  overflow-wrap: anywhere;
}
.facts dd small {
  display: block;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--color-muted);
}
/* Two rows of three, or three of two — never five and a widow. */
@media (min-width: 40rem) { .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 48rem) { .facts { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* ── Document rhythm ────────────────────────────────────────────
 *
 * `p` and the headings are set to `margin: 0` at the top of this file. That
 * is right for the composed pages: every component there owns its own
 * spacing, and a global paragraph margin would fight all of them.
 *
 * The prose pages are plain semantic markup written later, and they
 * inherited that zero with nothing to replace it. Measured on the served
 * page: section to section 0.2px, heading to its first paragraph 1px,
 * paragraph to paragraph 7.2px — and that last number is only the
 * half-leading of the line box, not a margin. So a section heading sat on
 * the paragraph above it and read as its last line, and two paragraphs ran
 * together as one. On /request/ an h3 overlapped the h2 above it by 0.7px.
 *
 * In the stylesheet all of that looks deliberate, which is why it survived
 * four pages and eight months. `scripts/qc_readability.py` found it by
 * measuring the painted text on the page instead of reading the rules.
 *
 * The rule the site was missing is asymmetry. A heading needs much more
 * space above it than below, because the space is what says which text the
 * heading belongs to. Equal space on both sides leaves it floating between
 * two sections; none at all attaches it to the one it is not about.
 *
 *     section → section   --space-xl    64px
 *     * → h3              --space-lg    40px
 *     p → p               --space-sm    16px   (0.94 of a line)
 *     heading → its text  --space-xs    12px
 *
 * `.prose` goes on whatever element holds the running prose: the `.wrap` on
 * a document page, or a single `<section>` on a page that is otherwise
 * composed, as /request/'s questions are. Both are handled, which is why
 * each rule below is written twice.
 *
 * It is opt-in rather than applied to every `.wrap > section`, because
 * /sample/ and /sample-tln/ put components in the same element —
 * `.artifacts`, `.refusals`, `.redaction` — and each of those already
 * carries its own top padding and rule. A blanket rule would double their
 * spacing, which is the same class of defect in the other direction. */
.prose > section + section { margin-top: var(--space-xl); }
/* And when `.prose` is itself the section — /request/'s questions — it
   needs the same gap from whatever composed section precedes it. It had
   20px, which on a page that has just ended a bordered block reads as a
   paragraph break rather than a new subject. */
section.prose { margin-top: var(--space-xl); }
/* A hero already pays 40px out of its own padding, so the first section was
   landing 104px below the effective date — a hole rather than a break. */
.prose > section:first-of-type { margin-top: var(--space-md); }

/* The UA gives a list 1em top AND bottom, and on the document pages that
   was the only rhythm present — which is why the spacing read as arbitrary
   rather than as missing. Above the rules that follow, not after them:
   `.prose ul` and `.prose > section > * + *` have the same specificity, so
   a reset written second silently cancels the rhythm for one element. */
.prose ul,
.prose ol { margin: 0; padding-left: 1.2rem; }
.prose li + li { margin-top: var(--space-2xs); }

.prose > * + *,
.prose > section > * + * { margin-top: var(--space-sm); }
.prose > * + :is(h3, h4),
.prose > section > * + :is(h3, h4) { margin-top: var(--space-lg); }
.prose > :is(h2, h3, h4) + *,
.prose > section > :is(h2, h3, h4) + * { margin-top: var(--space-xs); }

/* The effective date under the lede had 6px, which on a line of small grey
   type reads as a wrapped continuation of the sentence above it. */
.hero .meta { margin-top: var(--space-sm); }

/* Measure.
 *
 * The document pages — about, privacy, terms, cookies — are running prose in
 * a container 1216px wide, which is roughly 160 characters a line. The eye
 * loses its place returning to the left margin somewhere past 90. Constrain
 * the text elements rather than the container, so a table, a figure or a card
 * in the same section still gets the full width. */
.wrap > section > p,
.wrap > section > ul,
.wrap > section > ol,
.wrap > header > p:not(.hero__stages) { max-width: 68ch; }

/* ═══ The operator console ══════════════════════════════════════════════
 * Hallmark · genre: modern-minimal · macrostructure: Console (bespoke)
 * · theme: Cite Unseen house tokens (preserved, not rotated — a console
 *   that looked like a different product than the site it belongs to
 *   would be a worse outcome than a repeated macrostructure)
 * · nav: bespoke sticky work-header · footer: inherited site colophon
 * · enrichment: none · anchor hue: warm 22 · motion: none
 * · pre-emit critique: P5 H5 E4 S5 R5 V4
 *
 * Why bespoke rather than a catalogue pick: all twenty-one macrostructures
 * are page-shapes for something a visitor reads. This is a tool somebody
 * operates — twenty checks, an hour, usually on a phone, usually tired.
 * The shapes that matter here are a work header that never scrolls away, a
 * grid whose columns line up so twenty rows can be scanned rather than
 * read, and a decision panel that stays put while the evidence beside it
 * moves.
 *
 * Everything below is JavaScript-free. The site publishes
 * `script-src 'none'` and the privacy page rests on it. Filters are links,
 * disclosure is `<details>`, and the sticky panel is `position: sticky`.
 * ═══════════════════════════════════════════════════════════════════════ */

/* The console opts out of the marketing shell's centred fold. An operator
   reads a line, goes and looks at a website, comes back. Centred body copy
   is harder to come back to: the eye has to hunt for the left edge on
   every line. */
.console {
  --console-gutter: var(--space-md);
  text-align: left;
  max-width: 74rem;
  margin: 0 auto;
  padding: 0 var(--console-gutter) var(--space-xl);
}
.console *,
.console *::before,
.console *::after { box-sizing: border-box; }

/* ── The work header ─────────────────────────────────────────────────
   Sticks, because the two facts an operator loses track of are which site
   they are auditing and how much is left, and both were at the top of a
   page they had scrolled off. */
.wh {
  position: sticky;
  top: 0;
  z-index: 20;
  margin: 0 calc(-1 * var(--console-gutter)) var(--space-md);
  padding: var(--space-xs) var(--console-gutter) var(--space-2xs);
  background: var(--color-paper);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
.wh__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xs) var(--space-sm);
}
.wh__id { display: flex; align-items: baseline; gap: var(--space-2xs);
          flex-wrap: wrap; min-width: 0; }
.wh__host {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  overflow-wrap: anywhere;
  min-width: 0;
}
.wh__back {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
  text-decoration: none;
  white-space: nowrap;
}
.wh__back:hover { color: var(--color-ink); }
.wh__count {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  /* Counts that change as you work down a queue. Without tabular figures
     the number shifts sideways on every signature, which reads as the
     layout twitching. */
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
  white-space: nowrap;
}
.wh__count b { color: var(--color-ink); }

/* A meter, not a decorative bar. Signed and drafted are separate fills,
   because "17 of 20" that silently counts unfinished notes is a number
   that lies at exactly the moment it matters. */
.meter {
  display: flex;
  height: 4px;
  margin-top: var(--space-2xs);
  border-radius: 999px;
  overflow: hidden;
  background: var(--color-paper-3);
}
.meter__done { background: var(--color-good); }
.meter__live { background: var(--color-inconclusive); }

/* ── Cards ───────────────────────────────────────────────────────────
   One surface, one elevation, one radius. The reason the old surface read
   as a wall is that nothing was on top of anything: hairlines between
   equal things give the eye no order to follow. */
.card {
  background: var(--color-paper-2);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
}
.card__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xs) var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
.card__title {
  margin: 0;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.card__body { padding: var(--space-sm); }
/* The two-reader panel draws its own row separators, so the card body
   must not add a band of padding above the first one. */
.card__body--flush { padding-block: 0; }
.card__body > :first-child { margin-top: 0; }
.card__body h3.card__title { margin: var(--space-sm) 0 var(--space-3xs); }
.card__body > :last-child { margin-bottom: 0; }
.card + .card { margin-top: var(--space-md); }

/* ── Status ──────────────────────────────────────────────────────────
   Three states, three colours, literally: complete is green, in progress
   is blue, pending is red. Every pill carries the word as well as the
   hue — roughly one man in twelve reads red and green alike, and a status
   that lives only in a colour means nothing to him.

   The colours are tokens that already exist and already clear 4.5:1. A
   fourth near-identical blue invented here is the drift
   check_design_tokens.py was written to catch. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.15em 0.6em;
  border-radius: 999px;
  border: var(--rule-hair) solid currentColor;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: 0.03em;
  white-space: nowrap;
}
.pill::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}
.pill--done { color: var(--color-good); }
.pill--live { color: var(--color-inconclusive); }
.pill--open { color: var(--color-ink-2); }
/* A tier that exists but is not for sale. Muted rather than alarming: it
   is a roadmap note in a price table, not a fault. */
.tag--soon {
  display: inline-block;
  margin-left: var(--space-2xs);
  /* 0.1em was 1.2px at this size -- the word sat on its own border, which
     on a pill is the one place it shows. 0.35em/0.7em is the same 1:2 the
     report's pills use, and takes the tag from 18px tall to 24px, which is
     also the AA target floor this file holds links to. */
  padding: 0.35em 0.7em;
  border: var(--rule-hair) solid var(--color-rule-2);
  border-radius: 999px;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--color-muted);
  white-space: nowrap;
}

/* The marketing opt-in. A whole-row target, and never pre-ticked. */
.scanbox__check {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2xs);
  margin-top: var(--space-2xs);
  padding: var(--space-2xs);
  margin-inline: calc(-1 * var(--space-2xs));
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
  color: var(--color-ink-2);
  cursor: pointer;
}
.scanbox__check:hover { background: var(--color-paper-3); }
.scanbox__check input {
  flex: 0 0 auto;
  margin-top: 0.25em;
  accent-color: var(--color-accent-deep);
}

/* What this rating would cost the customer. Weight rather than a second
   hue: a third colour beside the status pill would compete with it, and
   the word already carries the meaning. */
.sev {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.sev--critical { color: var(--color-accent-deep); font-weight: 700; }
.sev--major { color: var(--color-ink-2); font-weight: 700; }

/* ── Segmented filter control ────────────────────────────────────────
   Links, not a <select>: a select with no script needs a submit button
   beside it, and a filter you press twice is a filter nobody uses. Each
   option is a URL, so a filtered view is also one you can send. */
.seg {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3xs);
  padding: var(--space-3xs);
  background: var(--color-paper-3);
  border-radius: var(--radius-sm);
}
.seg__item {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: 0 var(--space-xs);
  border-radius: calc(var(--radius-sm) - 2px);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: var(--color-neutral);
  text-decoration: none;
  white-space: nowrap;
}
.seg__item:hover { background: var(--color-paper-2); color: var(--color-ink); }
.seg__item[aria-current="page"] {
  background: var(--color-paper);
  color: var(--color-ink);
  box-shadow: var(--shadow-1);
}
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs) var(--space-sm);
  margin: 0 0 var(--space-md);
}
.toolbar__aside {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: var(--color-muted);
  text-decoration: none;
  white-space: nowrap;
}
.toolbar__aside:hover { color: var(--color-ink); }

/* ── The check list ──────────────────────────────────────────────────
   A grid, so twenty rows can be scanned down a column instead of read as
   twenty sentences. Cards per row would be twenty elevations fighting each
   other; one card holding aligned rows is the dashboard idiom and it is
   the right one. */
.rows { list-style: none; margin: 0; padding: 0; }
.rows > li + li { border-top: var(--rule-hair) solid var(--color-rule); }
.row {
  display: grid;
  grid-template-columns: 4.25rem minmax(7rem, max-content) minmax(0, 1fr);
  align-items: baseline;
  gap: var(--space-3xs) var(--space-sm);
  padding: var(--space-2xs) var(--space-sm);
  text-decoration: none;
  color: inherit;
}
.row:hover { background: var(--color-paper-3); }
.row:focus-visible { outline-offset: -2px; border-radius: var(--radius-sm); }
.row__id {
  font-family: var(--font-label);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--color-accent-deep);
}
.row__main { min-width: 0; }
.row__name {
  display: block;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.row__why {
  display: block;
  margin-top: 2px;
  font-size: var(--text-sm);
  color: var(--color-muted);
}
.row__flags { display: flex; align-items: center; gap: var(--space-2xs);
              flex-wrap: wrap; }
@media (max-width: 39.99rem) {
  .row { grid-template-columns: 1fr auto; }
  .row__main { grid-column: 1 / -1; }
}

/* ── The queue ───────────────────────────────────────────────────────
   Auto-fit rather than a fixed column count: a queue of one audit should
   not render one card in a three-column grid with two holes in it. */
.queue {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  gap: var(--space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}
.qcard {
  display: block;
  padding: var(--space-sm);
  background: var(--color-paper-2);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
  text-decoration: none;
  color: inherit;
}
.qcard:hover { box-shadow: var(--shadow-2); border-color: var(--color-rule-2); }
.qcard:focus-visible, .seg__item:focus-visible { outline-offset: 2px; }
.qcard__host {
  margin: 0 0 var(--space-3xs);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  overflow-wrap: anywhere;
}
.qcard__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  margin: 0 0 var(--space-2xs);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
}

/* ── The check page: evidence left, decision right ───────────────────
   Two columns only where two columns fit. Below that the decision panel
   goes last, which is the order the work happens in: read, look, decide. */
.work { display: grid; gap: var(--space-md); align-items: start; }
@media (min-width: 62rem) {
  .work { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); }
  .work__decide { position: sticky; top: 5.5rem; }
}

.lede {
  margin: 0 0 var(--space-sm);
  font-size: var(--text-md);
  line-height: var(--lh-body);
  max-width: var(--measure);
}
.meta { color: var(--color-muted); font-size: var(--text-sm); }
.limits {
  margin: var(--space-2xs) 0 0;
  padding-left: var(--space-md);
  color: var(--color-ink-2);
  font-size: var(--text-sm);
}

/* Two readers, shown as two. Collapsing them into one number throws away
   the only signal in the system that came from disagreement. */
.reader {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: var(--space-3xs) var(--space-sm);
  padding: var(--space-xs) 0;
}
.reader + .reader { border-top: var(--rule-hair) solid var(--color-rule); }
.reader__who {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.reader__said { margin: 0; font-weight: 600; }
.reader__note { margin: var(--space-3xs) 0 0; font-size: var(--text-sm);
                color: var(--color-ink-2); }
@media (max-width: 39.99rem) {
  .reader { grid-template-columns: 1fr; }
}
/* A disagreement is the one thing on this page worth its own colour: it
   is where the reviewer's hour actually pays. */
.card--split { border-color: var(--color-rule-2); }
.verdict {
  margin: 0;
  /* The global `p { max-width: var(--measure) }` keeps prose readable and
     is right everywhere else. This is a full-bleed band at the foot of a
     card, and a band that stops two thirds of the way across reads as a
     rendering bug. */
  max-width: none;
  padding: var(--space-xs) var(--space-sm);
  border-top: var(--rule-hair) solid var(--color-rule);
  background: var(--color-paper-3);
  font-size: var(--text-sm);
  border-radius: 0 0 calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px);
}

/* The delivery preview's summary. A definition list read as a stack of
   loose pairs; on a grid the labels line up and the numbers can be
   compared down a column, which is the whole reason they are together. */
.console .facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-xs) var(--space-sm);
  margin: 0 0 var(--space-sm);
}
.console .facts dt {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.console .facts dd {
  margin: 2px 0 0;
  font-size: var(--text-md);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* ── The playbook ────────────────────────────────────────────────────
   What a reviewer needs before they read anything else, so it sits above
   the evidence and opens by default: somebody who does not know what they
   are looking for will not think to open the panel that would tell them. */
.steps {
  margin: var(--space-2xs) 0 var(--space-sm);
  padding-left: 1.35rem;
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}
.steps li { margin-bottom: var(--space-2xs); }
.steps li::marker { font-family: var(--font-label); color: var(--color-muted); }

/* The decision table. A rating stops being a feeling and becomes a
   lookup: read down until a row fits, take that rating. Two columns so
   the ratings stack in one scannable line and the conditions sit beside
   them; one column on a phone, where they would otherwise be two words
   wide. */
.decide { border-top: var(--rule-hair) solid var(--color-rule); }
.decide__row {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: var(--space-2xs) var(--space-sm);
  padding: var(--space-2xs) var(--space-sm);
  font-size: var(--text-sm);
}
.decide__row + .decide__row {
  border-top: var(--rule-hair) solid var(--color-rule);
}
.decide__rating {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-ink);
  font-weight: 700;
}
.decide__when { color: var(--color-ink-2); }
@media (max-width: 39.99rem) {
  .decide__row { grid-template-columns: 1fr; gap: var(--space-3xs); }
}

/* Paging between checks, in the work header beside the count. */
.stepnav { display: inline-flex; gap: var(--space-2xs);
           margin-right: var(--space-sm); }
.stepnav__go, .stepnav__end {
  display: inline-flex;
  align-items: center;
  min-height: 2rem;
  padding: 0 var(--space-2xs);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-sm);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  text-decoration: none;
  white-space: nowrap;
}
.stepnav__go { color: var(--color-ink); background: var(--color-paper-2); }
.stepnav__go:hover { background: var(--color-paper-3); }
/* The end of the list is shown, not hidden. A control that disappears at
   the boundary makes the row jump and leaves the reviewer wondering
   whether they mis-tapped. */
.stepnav__end { color: var(--color-muted); opacity: 0.5; }

/* ── Evidence ────────────────────────────────────────────────────────
   `<details>` because script-src 'none' is load-bearing and a review page
   is not where that changes. */
.ev { border-top: var(--rule-hair) solid var(--color-rule); }
.ev:first-child { border-top: 0; }
.ev > summary {
  padding: var(--space-xs) var(--space-sm);
  cursor: pointer;
  font-size: var(--text-sm);
}
.ev > summary:hover { background: var(--color-paper-3); }
.ev__url { font-family: var(--font-mono); word-break: break-all;
           color: var(--color-ink); }
.ev__facts {
  display: block;
  margin-top: 2px;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  color: var(--color-muted);
}
.ev__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0 var(--space-sm) var(--space-2xs);
  font-size: var(--text-xs);
}
/* Somebody else's markup, shown as text and never as markup. It wraps
   rather than scrolls sideways: this gets read on a phone. */
.ev__body {
  margin: 0 var(--space-sm) var(--space-sm);
  padding: var(--space-xs);
  max-height: 20rem;
  overflow: auto;
  background: var(--color-paper);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}

/* ── The decision form ───────────────────────────────────────────────
   Whole-row tap targets. A radio list that wraps mid-label is the control
   people mis-tap on a phone, and this is used on a phone by design. */
.opset { border: 0; margin: 0 0 var(--space-sm); padding: 0; }
.opset:last-of-type { margin-bottom: var(--space-2xs); }
.opset legend {
  padding: 0;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.opt {
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
  min-height: 2.5rem;
  padding: var(--space-3xs) var(--space-2xs);
  margin: 0 calc(-1 * var(--space-2xs));
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.opt:hover { background: var(--color-paper-3); }
.opt:has(input:checked) { background: var(--color-paper-3); }
.opt input { flex: 0 0 auto; accent-color: var(--color-accent-deep); }
/* The rating name stays on one line. A clickable label that wraps
   mid-phrase is the control people mis-tap, and this one is tapped
   twenty times an audit on a phone. */
.opt > b { white-space: nowrap; }
.opt__why { color: var(--color-ink-2); font-size: var(--text-sm); }
.opt--inline { display: inline-flex; min-height: 2.75rem;
               margin-right: var(--space-2xs); }
.field { display: block; margin-bottom: var(--space-2xs); }
/* A review note is prose that a customer reads, not code. It was
   inheriting the scan box's monospace, which made every reason look like
   a log line. */
/* A URL is read character for character, and JetBrains Mono renders `://`
   as a ligature that looks like a colon, a space and a slash. Chris typed
   a correct address and the field appeared to have eaten one of the
   slashes. Ligatures are for prose; anything showing a literal string
   turns them off. */
input, textarea, code, pre, .ev__url, .row__id {
  font-variant-ligatures: none;
  font-feature-settings: "kern";
}

textarea.scanbox__input {
  width: 100%;
  max-width: none;
  resize: vertical;
  line-height: 1.5;
  font-family: var(--font-body);
  font-size: var(--text-sm);
}
.actions { display: flex; flex-wrap: wrap; gap: var(--space-2xs);
           margin-top: var(--space-xs); }
.actions .btn { flex: 1 1 auto; }
/* A draft is not a signature and must not look like one. Outlined against
   the filled primary, and second in the source order so the keyboard
   reaches Sign first. */
.btn--quiet {
  background: transparent;
  color: var(--color-ink);
  border: var(--rule-hair) solid var(--color-rule-2);
}
.btn--quiet:hover { background: var(--color-paper-3); }

.emptystate {
  padding: var(--space-lg) var(--space-sm);
  text-align: center;
  color: var(--color-ink-2);
}
.emptystate p { margin: 0 auto; max-width: 46ch; }

/* Paid and not delivered. First on the page and the only card with the
   accent rule, because it is the one list where somebody is out of pocket
   and waiting. */
.paid { border-left: 3px solid var(--color-rule-2); }
.paid__h {
  margin: 0 0 var(--space-2xs);
  font-size: var(--step-1);
  line-height: 1.2;
}
.paid__rows { list-style: none; margin: var(--space-sm) 0 0; padding: 0; }
.paid__row {
  display: grid;
  gap: var(--space-3xs) var(--space-sm);
  padding: var(--space-xs) 0;
  border-top: 1px solid var(--color-rule);
}
.paid__site { font-weight: 600; color: var(--color-ink); overflow-wrap: anywhere; }
.paid__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3xs) var(--space-xs);
  align-items: center;
  font-size: var(--step--1);
  color: var(--color-ink-2);
}
@media (min-width: 48rem) {
  .paid__row {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
  }
  .paid__row .row__id { grid-column: 1 / -1; }
}

/* The mailing list, noted under the queue rather than given a card of its
   own: it is a standing fact about the operation, not a piece of work
   waiting to be done, and a card would rank it alongside audits. */
.listnote {
  margin: var(--space-md) 0 0;
  font-size: var(--step--1);
  color: var(--color-ink-2);
}

@media (prefers-reduced-motion: reduce) {
  .console * { transition: none !important; }
}

/* D74c — Monitoring stays in the list so a buyer knows it is coming, and
   carries no figure, because a price nobody can pay is a quote we would have
   to honour or explain. Set at the weight of the copy beside it rather than
   the weight of a price. */
.nudge__price--tbc,
.tier__price--tbc,
.price--tbc {
  color: var(--color-ink-2);
  font-family: var(--font-label);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0;
}

/* ── The readout ───────────────────────────────────────────────
   The one layout rule this refresh adds, and it is the product thesis
   rather than a decoration: a number the tool derived is set in the mono
   face, and the line under it names where it came from. A figure with no
   source line is a figure that does not belong on this site. */
.readout {
  margin-top: var(--space-md);
  border-top: var(--rule-hair) solid var(--color-rule);
  padding-top: var(--space-sm);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.readout__row {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: var(--space-sm);
  align-items: baseline;
}
.readout__fig {
  font-family: var(--font-label);
  font-size: var(--text-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  color: var(--color-ink);
}
.readout__what { font-size: var(--text-sm); color: var(--color-ink-2); }
.readout__src {
  display: block;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
  margin-top: 2px;
}
@media (max-width: 39.99rem) {
  .readout__row { grid-template-columns: 1fr; gap: 0; }
}

/* ── The rubric document ───────────────────────────────────────
   Two columns from 62rem: the eight dimensions on the left, sticky, with
   the current one marked; the dimension itself on the right. Below that
   the rail becomes a wrapping row above the content, which is where a
   reader on a phone expects a section index. */
.doc { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-md); }
@media (min-width: 62rem) {
  .doc { grid-template-columns: 14rem minmax(0, 1fr); gap: var(--space-xl); }
  .doc__rail { position: sticky; top: var(--space-sm); align-self: start; }
}
.doc__rail { display: flex; flex-wrap: wrap; gap: var(--space-3xs) var(--space-2xs); }
@media (min-width: 62rem) { .doc__rail { flex-direction: column; flex-wrap: nowrap; gap: 0; } }
.doc__rail-link, .doc__rail-all {
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
  min-height: 2.75rem;
  padding: var(--space-3xs) var(--space-2xs);
  font-size: var(--text-sm);
  color: var(--color-ink-2);
  text-decoration: none;
  border-bottom: 2px solid var(--color-rule);
}
@media (min-width: 62rem) {
  .doc__rail-link, .doc__rail-all {
    border-bottom: none;
    border-left: 2px solid var(--color-rule);
    padding: var(--space-3xs) 0 var(--space-3xs) var(--space-xs);
  }
}
.doc__rail-link:hover, .doc__rail-all:hover { color: var(--color-ink); }
.doc__rail-link.is-here {
  color: var(--color-ink);
  font-weight: 600;
  border-color: var(--color-accent-deep);
}
.doc__rail-pts {
  margin-left: auto;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
}
.doc__rail-all { margin-top: var(--space-2xs); font-family: var(--font-label);
  font-size: var(--text-xs); letter-spacing: var(--tracking-label);
  text-transform: uppercase; }
/* The dimension pages are running prose and were outside the rhythm: a
   heading sat 17px under the paragraph above it and 0px above its own
   text, so it read as that paragraph's last line rather than as the title
   of what follows. `.prose` supplies the asymmetry; this column supplies
   the section rhythm through its flex gap.
   The gap is raised to --space-xl to match `.prose > section + section`,
   and that margin is zeroed here rather than left to stack: a margin on a
   flex item ADDS to the container's gap, so keeping both gave 104px
   between sections — the double-spacing the note above this block warns
   about, arriving by the other route. */
.doc__body { min-width: 0; display: flex; flex-direction: column; gap: var(--space-xl); }
.doc__body.prose > section + section { margin-top: 0; }

/* The hub list. Each item is a question somebody types, the answer under
   it, and what the dimension costs. Ruled rather than carded, for the same
   reason the stages and tiers are. */
.hub__list { list-style: none; margin: var(--space-md) 0 0; padding: 0;
  border-top: var(--rule-hair) solid var(--color-rule); }
.hub__list li {
  border-bottom: var(--rule-hair) solid var(--color-rule);
  padding: var(--space-sm) 0;
  display: grid;
  gap: var(--space-3xs);
}
.hub__list a { font-size: var(--text-md); font-weight: 600; text-decoration: none;
  color: var(--color-ink); text-decoration-color: var(--color-rule); }
.hub__list a:hover { text-decoration: underline; }
.hub__meta { font-family: var(--font-label); font-size: var(--text-xs);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-muted); }
.hub__ans { font-size: var(--text-base); color: var(--color-ink-2); max-width: 62ch; }

/* The hub filter. Inserted by site.js, so these rules style something that
   is absent without it: the list is the content and the filter is the
   convenience, which is the order they degrade in. */
.hub__filter {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  margin-top: var(--space-md);
}
.hub__filter-label {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.hub__filter-input {
  min-height: 2.75rem;
  padding: 0.6rem var(--space-sm);
  font-family: var(--font-label);
  font-size: var(--text-base);
  color: var(--color-ink);
  background: var(--color-paper-2);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-sm);
  max-width: 26rem;
}
.hub__filter-count {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
  margin: 0;
}


/* ── The collapsing menu (D80d) ──────────────────────────────
 * Below 48rem the nav groups sit behind one disclosure; above it, the
 * summary disappears and the groups lay out as they always did.
 *
 * The override on the closed state is the load-bearing line. A closed
 * <details> hides its non-summary children, and above 48rem there is no
 * disclosure to open, so without this the whole nav would vanish on
 * desktop for anyone who had collapsed it on a phone.
 */
/* NOT `display: contents` here. A <details> with `display: contents`
   generates no box, and the browser then renders its children whether it is
   open or closed -- the disclosure looked right and collapsed nothing. It
   takes `contents` only above 48rem, where there is no disclosure and the
   groups need to flow in the nav's own flex row. */
.nav__more { display: block; }
/* And the closed state is stated rather than inherited. A closed <details>
   hides its content through the UA stylesheet, which an author rule with a
   real `display` on the child overrides: `.nav__groups` is `display: flex`,
   so the menu rendered open-looking while `details.open` was false. Saying
   it here is the only version that survives the cascade. */
.nav__more:not([open]) > .nav__groups { display: none; }
.nav__more-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 2.75rem;
  padding: 0 0.9rem;
  border: var(--rule-hair) solid var(--color-rule-2);
  border-radius: var(--radius-pill);
  color: var(--color-ink);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  cursor: pointer;
  list-style: none;
}
.nav__more-btn::-webkit-details-marker { display: none; }
.nav__more-btn::after { content: "+"; font-size: 1.1em; line-height: 1; }
.nav__more[open] > .nav__more-btn::after { content: "\2212"; }

@media (min-width: 48rem) {
  .nav__more { display: contents; }
  .nav__more-btn { display: none; }
  /* Open or closed, the groups are a row on a wide screen.
     `display` on the child is NOT enough and shipping it that way hid the
     whole menu on every desktop width: a closed <details> has its content
     hidden by the UA through `content-visibility: hidden` on the
     `::details-content` pseudo, which suppresses descendants whatever
     `display` they compute. The pseudo is what has to be overridden.
     The `display` rule below stays for engines that predate the pseudo
     and hide the content the old way. */
  .nav__more::details-content {
    content-visibility: visible;
    display: contents;
  }
  .nav__more:not([open]) > .nav__groups { display: flex; }
}

/* The phone masthead was TALLER than the desktop one: 199px against 115px,
   measured in Chrome at 320, 390, 768 and 1280. The device with the least
   vertical space carried the most chrome, 23.6% of an 844px viewport, on
   every page, before a word of content -- and it is what pushed the scan
   form down into the consent bar.

   Three stacked rows rather than two: the mark and the theme control, then
   `Menu` alone, then `Your audits` and the call to action. `Menu` had a
   whole row to itself and the row under it was half empty, so they become
   one row.

   This sits below the rules it overrides on purpose. Written above them it
   did nothing, because a media query adds no specificity and the
   unconditional `.nav { flex-wrap: wrap }` is further down the file. */
@media (max-width: 47.99rem) {
  .nav {
    flex-wrap: nowrap;
    gap: var(--space-2xs);
    justify-content: space-between;
  }
  .nav__more { flex: 0 0 auto; }
  /* `width: auto` is load-bearing: an earlier phone rule sets
     `.nav__end { width: 100% }`, and with flex-basis auto that width
     still won, so the row stretched and pushed the call to action off
     the right edge where it read as "Run a f". */
  .nav__end { gap: var(--space-2xs); flex: 0 0 auto; width: auto; }
}
/* Below 22.5rem the three controls genuinely do not fit on one line. The
   account link is the one to give up: it is a destination rather than an
   action, every footer carries it, and somebody who has audits knows where
   they are. The call to action is not negotiable. */
@media (max-width: 22.49rem) {
  .nav__account { display: none; }
}


/* ── The fold's order on a phone (D80e) ─────────────────────
 * The scan form is the one action this site exists for, and it sat below
 * the headline, the lede and three readouts: 1173px down a 568px screen at
 * 320px. The readouts are the argument for running a scan, which is worth
 * reading -- after being offered the scan, not before it.
 *
 * So on a phone the order is headline, lede, form, evidence. Above 62rem
 * the two-column fold puts the form beside the argument and the source
 * order is already right, so this applies only where the layout stacks.
 *
 * `order` rather than moving the markup: the reading order for a screen
 * reader and the tab order both follow the DOM, and headline-then-form is
 * the right sequence in both. This only moves the readouts down visually.
 */
@media (max-width: 61.99rem) {
  .fold { display: flex; flex-direction: column; }
  /* D80e moved the form above the readouts and the demo. It left the lede
     between the headline and the form, and on a short phone that is what
     still pushed the button under the fold: 133px over on a 375x667 SE and
     26px over on a 360x800. The two common sizes (390x844, 393x852) were
     already fine, so this is a small-screen fix, not a redesign.
     The lede is the same kind of thing the readouts are -- an argument for
     scanning -- so it goes where D80e put them, after the offer.
     `display: contents` lifts the headline and the lede out of their
     wrapper so `order` can separate them. It is scoped to this media query,
     so the two-column grid above 62rem never sees it, and `.fold__arg`
     carries nothing but `grid-area` and `min-width` -- no padding,
     background or border is lost by dissolving the box.
     DOM order is untouched, so a screen reader and the tab key still get
     headline, lede, form. Only the paint moves. */
  .fold__arg  { display: contents; }
  .fold__slogan { order: 1; }
  .fold__act  { order: 2; }
  .fold__lede { order: 3; }
  /* The demo is the argument for scanning, and it goes after the offer to
     scan for the same reason the readouts do. Putting it first cost 376px
     and pushed the form back below the fold on every phone. */
  .fold .demo { order: 4; }
  .fold .readout { order: 5; }
}


/* ── The breadcrumb (D81) ────────────────────────────────────
 * On the eight dimension pages only. They are the site's only L2 pages and
 * the only ones a reader can arrive at from search with no idea what sits
 * above them.
 *
 * The BreadcrumbList schema has been in the head since these pages were
 * written and nothing rendered it, so a crawler could read the trail and a
 * reader could not. Both now come from one list in build_site.
 */
.crumbs { margin-bottom: var(--space-md); }
.crumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
/* The separator is drawn rather than typed, so a screen reader announces
   three list items and not three slashes. */
.crumbs li + li::before {
  content: "";
  display: inline-block;
  width: 0.55rem;
  height: 1px;
  margin-right: var(--space-2xs);
  vertical-align: middle;
  background: var(--color-rule-2);
}
.crumbs a { color: var(--color-ink-2); }
.crumbs li:last-child { color: var(--color-muted); }


/* ── The rubric's weights (D81) ──────────────────────────────
 * One measure across eight categories, so it is a ranked bar with a single
 * hue. Eight colours would be encoding identity that the label already
 * carries and magnitude that the length already carries.
 *
 * Every value is a token, so the same markup serves Ember and the light
 * theme and a palette change takes the chart with it. The text wears ink
 * tokens rather than the bar colour: the bar carries identity, the words
 * carry the words.
 */
.weights__fig { margin: var(--space-lg) 0 0; }
.weights {
  display: block;
  width: 100%;
  max-width: 35rem;
  height: auto;
  overflow: visible;
}
.weights__track { fill: var(--color-paper-3); }
.weights__bar   { fill: var(--color-accent-deep); }
.weights__name {
  fill: var(--color-ink-2);
  font-family: var(--font-body);
  font-size: 12px;
}
.weights__val {
  fill: var(--color-ink);
  font-family: var(--font-label);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.weights__fig figcaption {
  margin-top: var(--space-sm);
  color: var(--color-neutral);
  font-size: var(--text-sm);
  max-width: 58ch;
}
/* Below 40rem the labels stop fitting beside the bars at a readable size.
   The chart is supplementary -- the eight links under it carry the same
   numbers -- so it steps back rather than shrinking its text to 8px. */
@media (max-width: 39.99rem) {
  .weights__name { font-size: 10px; }
}


/* ── The wait (D81d) ─────────────────────────────────────────
 * Twenty silent seconds sat at the exact moment of conversion, and the only
 * feedback was a label that said "Scanning…".
 *
 * The bar is INDETERMINATE on purpose. The scan is a synchronous POST with
 * no state to poll, so a determinate bar would animate on a timer and
 * measure nothing -- which is precisely the kind of claim this product
 * audits other people for. It says "a wait is happening", and the counter
 * beside it says how long, which is a real number.
 */
.waiting {
  margin-top: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-paper-2);
}
.waiting__bar {
  height: 3px;
  border-radius: 999px;
  background: var(--color-rule);
  overflow: hidden;
}
.waiting__bar span {
  display: block;
  width: 34%;
  height: 100%;
  border-radius: inherit;
  background: var(--color-accent-deep);
  animation: waiting-sweep 1.4s ease-in-out infinite;
}
@keyframes waiting-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(392%); }
}
/* Reduced motion gets a static bar rather than no bar: the panel still has
   to say that something is happening. */
@media (prefers-reduced-motion: reduce) {
  .waiting__bar span { animation: none; width: 100%; opacity: 0.55; }
}
.waiting__stages {
  margin: var(--space-sm) 0 0;
  padding-left: 1.1rem;
  display: grid;
  gap: var(--space-3xs);
  font-size: var(--text-sm);
  color: var(--color-ink-2);
}
.waiting__stages code { font-family: var(--font-mono); font-size: 0.92em; }
.waiting__clock {
  margin-top: var(--space-xs);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}


/* ── The demo (D81e) ─────────────────────────────────────────
 * Self-hosted, because `default-src 'self'` blocks an embedded player and
 * the privacy page promises no third party sees a request for a page read
 * here. Two formats: VP9 for everything current, H.264 for Safari old
 * enough to want it. The poster paints first either way, so the fold is
 * never empty while the file arrives.
 *
 * `muted autoplay loop playsinline` with no controls: it is twelve silent
 * seconds of the product working, not something to operate. The written
 * account in the figcaption is the content for anyone who does not or
 * cannot watch it, and is what keeps CI-01 passing.
 */
.demo { margin: 0 0 var(--space-lg); }
/* The controls only appear when site.js has added them, which it does only
   under `prefers-reduced-motion: reduce`. Sized so the still does not jump
   when they arrive. */
.demo__v[controls] { background: var(--color-paper-2); }
.demo__v {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
  aspect-ratio: 960 / 600;
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-paper-2);
}
.demo__cap {
  margin-top: var(--space-sm);
  color: var(--color-neutral);
  font-size: var(--text-sm);
  max-width: 62ch;
}
.demo__cap b { color: var(--color-ink-2); }
.demo__cap code { font-family: var(--font-mono); font-size: 0.92em; }

/* Reduced motion is handled in site.js, not here, and the first version of
   this block got that wrong: it set `animation: none` on the video and the
   comment claimed the loop would not start. CSS cannot stop video playback.
   A looping autoplay video is motion nobody asked for, and stopping it
   takes script -- so site.js pauses it and turns on controls, and this is
   the styling for the state that leaves. */
@media (prefers-reduced-motion: reduce) {
  .demo__v { outline-offset: 2px; }
}
@media (min-width: 62rem) {
  /* Its own full-width row under the fold. It shared `act` with the scan
     form for one build, which puts two elements in one grid area and
     stacks them on top of each other. */
  .demo { grid-area: demo; margin: var(--space-lg) 0 0; }
  .demo__v { max-width: 46rem; }
}


/* ── The tier's action row (operator feedback, 2026-09-14) ──────────────
 * "Buy this" was a text arrow sitting at the same weight as "or ask
 * first", so the page offered two equal options where one is the point.
 * The purchase is a button and the question stays a link beside it. */
.tier__act {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  margin-top: var(--space-sm);
}
/* The same fill the scan button uses, and for the same reason:
   `--color-accent-deep` is the step measured for filled buttons (7.01:1)
   and `--color-accent-ink` is the token for text sitting on an ember fill.
   Both flip with the theme, which `--color-paper` on `--color-accent`
   would not have — accent in light is a large-display step at 4.11:1. */
.btn--buy {
  background: var(--color-accent-deep);
  border-color: var(--color-accent-deep);
  color: var(--color-accent-ink);
}
.btn--buy:hover,
.btn--buy:focus-visible {
  background: var(--color-accent);
  border-color: var(--color-accent);
}


/* ── The consent banner ────────────────────────────────────────────────
 * Anchored bottom-left rather than across the whole viewport: a full-width
 * bar over the fold is the pattern people have learned to dismiss without
 * reading, and the fold is where the scan box lives. It never covers the
 * form on a phone because it sits below it in the flow of the screen, and
 * it is dismissed by answering rather than by waiting.
 *
 * Not `position: fixed` with a scrim: this is not modal. A reader who
 * wants to read the cookies page before answering should be able to.
 */
.consent {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  display: block;
  padding: var(--space-2xs) var(--space-sm);
}
/* A bar, not a panel. D91a.
   It was a 30rem box of a heading, three sentences, a link and two buttons
   all stacked, which measured 311px tall at 320px wide. The consequence was
   measured rather than felt: on the home page at 320x844 the free scan form
   ran y=536 to y=894 and this covered y=517 to y=828, so the control the
   whole page exists for sat behind a question about analytics. Above 44rem
   the words and the choice sit side by side and this is one row; below it
   they stack and it is two. */
.consent__box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs) var(--space-md);
  max-width: var(--sheet-max);
  margin: 0 auto;
  padding: var(--space-2xs) var(--space-sm);
  border: var(--rule-hair) solid var(--color-rule-2);
  border-radius: var(--radius-sm);
  background:
    linear-gradient(rgba(0, 0, 0, 0.35), rgba(0, 0, 0, 0.35)),
    var(--color-paper-2, var(--color-paper));
  backdrop-filter: blur(13px);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.45);
}
:root[data-theme="light"] .consent__box {
  background: var(--color-paper);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
}
/* The words run together as one paragraph rather than stacking as four
   blocks, which is where most of the 311px went. */
.consent__say { flex: 1 1 17rem; min-width: 0; }
.consent__h {
  display: inline;
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
}
.consent__h::after { content: ". "; }
.consent__p {
  display: inline;
  margin: 0;
  font-size: var(--text-sm);
  color: var(--color-ink-2);
}
/* Not the accent. This link and the confirm button below were two of the
   four accent-family uses above the fold on EVERY page -- the whole budget
   the brand rule allows, spent before a page had said anything of its own.
   D92. */
.consent__more {
  /* Was `display: inline` at 18px tall -- under the 24px WCAG 2.2 AA floor
     the rest of this file holds itself to (see `.link-arrow`). A link in a
     sentence is exempt from 2.5.8, and this one is technically in a
     sentence, but the sentence is a consent bar: the whole surface is a
     control, and "what is actually stored" is the one thing a person
     hesitating over consent reaches for. Height added the same way
     `.link-arrow` adds it, below the baseline, so the underline stays on
     the words. */
  display: inline-flex;
  align-items: flex-start;
  min-height: 1.5rem;
  margin-left: 0.35em;
  font-size: var(--text-sm);
  color: var(--color-ink);
  white-space: nowrap;
}
.consent__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-xs);
  margin: 0;
  flex: 0 0 auto;
}
/* The two answers weigh the same, and neither is the accent.
   This was a filled accent button sitting directly under `Scan it`, which
   is also a filled accent button, so the page offered two maximum-emphasis
   actions and the one it exists for was the smaller of the two. The first
   replacement filled it with `--color-ink`, which in Ember is near-white:
   that made it the brightest thing on the page and simply moved the
   problem. Equal weight is also the safer answer on a consent surface,
   where an accept that outshouts the decline is the specific pattern
   regulators name. */
.consent__yes {
  min-height: 2.75rem;
  padding: 0.6rem 1.1rem;
  border-radius: var(--radius-sm);
  background: var(--color-paper-3);
  border: var(--rule-hair) solid var(--color-rule-2);
  color: var(--color-ink);
  cursor: pointer;
  font: inherit;
  font-family: var(--font-display);
  font-weight: 600;
}
.consent__yes:hover { border-color: var(--color-ink); }
/* "No thanks" is a real button at a real size, not grey small print. A
   decline that is harder to hit than the accept is a dark pattern, and it
   is the specific one regulators name. */
.consent__no {
  min-height: 2.75rem;
  padding: 0.6rem 1.1rem;
  border: var(--rule-hair) solid var(--color-rule-2);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-ink);
  font: inherit;
  font-family: var(--font-display);
  font-weight: 600;
  cursor: pointer;
}
.consent__no:hover { border-color: var(--color-ink); }

/* The last few pixels, and they are the ones that matter. On the two
   smallest viewports the bar still clipped `Scan it` by single digits --
   9px at 320x844 and 7px at 360x780 -- which is the same defect as covering
   the whole form, just harder to see. Padding is what was left to give, and
   the roomier spacing stays everywhere it fits. Verified by measuring the
   button and the bar at 320, 360, 390, 414, 768 and 1280. */
@media (max-width: 24rem), (max-height: 800px) {
  .consent { padding: 0.25rem var(--space-2xs); }
  .consent__box { padding: 0.25rem var(--space-2xs); gap: 0.25rem var(--space-sm); }
}

@media (prefers-reduced-motion: no-preference) {
  .consent__box { animation: consent-in 180ms ease-out; }
}
@keyframes consent-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}


/* ── The pricing plans (operator feedback, 2026-09-14) ──────────────────
 * Was a three-row table of prices and, some distance below it, a dropdown
 * that repeated two of them. A buyer had to read the table, scroll, open a
 * menu and match a label to a row from memory. The cards are the control:
 * the radio is the same one the form always posted, so the checkout
 * contract is unchanged and the page still works with no script at all.
 *
 * Selected state hangs off `input:checked ~ .plan__body` rather than
 * `:has()`, so it needs no modern selector support to show which one you
 * are buying. */
.plans {
  display: grid;
  gap: var(--space-md);
  border: 0;
  /* The first field sat flush against the cards, so the chooser and the
     form read as one undivided block. */
  margin: 0 0 var(--space-lg);
  padding: 0;
  min-width: 0;                 /* a fieldset will not shrink without it */
}
@media (min-width: 52rem) {
  .plans { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
}
.plans__legend {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.plan { display: block; cursor: pointer; height: 100%; }
.plan--soon { cursor: default; }

/* Off-screen rather than display:none: a hidden radio is not focusable,
   and the card then cannot be reached or chosen by keyboard at all. */
.plan__radio {
  position: absolute;
  opacity: 0;
  width: 1px; height: 1px;
  margin: 0;
}

.plan__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  height: 100%;
  padding: var(--space-md);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-sm);
  background: var(--color-paper-2, transparent);
  transition: border-color 120ms ease, background 120ms ease;
}
.plan:hover .plan__body { border-color: var(--color-rule-2); }
.plan__radio:checked ~ .plan__body {
  border-color: var(--color-accent-deep);
  box-shadow: inset 0 0 0 1px var(--color-accent-deep);
}
/* The focus ring belongs on the card, because the input it belongs to is
   a pixel in the corner. */
.plan__radio:focus-visible ~ .plan__body {
  outline: 2px solid var(--color-accent-deep);
  outline-offset: 3px;
}
.plan--soon .plan__body { opacity: 0.72; border-style: dashed; }

.plan__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
}
.plan__name { font-family: var(--font-display); font-weight: 600; font-size: var(--text-md); }

/* An empty circle that fills when the card is the chosen one. It is the
   only thing on the card that says "selected" without relying on colour. */
.plan__tick {
  flex: 0 0 auto;
  width: 1.15rem; height: 1.15rem;
  border-radius: 50%;
  border: 2px solid var(--color-rule-2);
}
.plan__radio:checked ~ .plan__body .plan__tick {
  border-color: var(--color-accent-deep);
  background:
    radial-gradient(circle at 50% 50%,
      var(--color-accent-deep) 0 45%, transparent 46%);
}

.plan__price {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  line-height: 1.05;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin-top: var(--space-2xs);
}
.plan__price--tbc { font-size: var(--text-md); color: var(--color-neutral); }
.plan__per {
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.plan__pitch { color: var(--color-ink-2); margin-top: var(--space-2xs); }

.plan__list { display: flex; flex-direction: column; gap: var(--space-2xs); margin-top: var(--space-xs); }
.plan__item {
  position: relative;
  padding-left: 1.4em;
  font-size: var(--text-sm);
  color: var(--color-ink-2);
}
.plan__item::before {
  content: "";
  position: absolute;
  left: 0.15em; top: 0.55em;
  width: 0.5em; height: 0.5em;
  border-radius: 50%;
  background: var(--color-accent-deep);
}
.plan--soon .plan__item::before { background: var(--color-rule-2); }
.plan__ask { margin-top: auto; padding-top: var(--space-sm); align-self: flex-start; }

.buy__lede { max-width: 58ch; color: var(--color-neutral); margin-top: var(--space-xs); }
.buy .form { margin-top: var(--space-lg); }


/* Forms (request, contact). Shared here rather than per page: a form that
   only one page styled rendered as bare browser controls on /contact/.
   A raised card with soft, rounded fields: the field is where the eye
   rests, so it gets the space, and the label sits tight above it. */
.form {
  max-width: 36rem;
  margin: var(--space-md) 0 0;
  padding: var(--space-lg);
  text-align: left;
  background: var(--color-paper-2);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-lg);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.12), 0 12px 32px -12px rgb(0 0 0 / 0.35);
}
.form__row { display: grid; gap: 0.4rem; margin: 0; }
.form__row + .form__row { margin-top: var(--space-md); }
.form__label {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--color-ink);
}
.form__hint { margin: 0; font-size: var(--text-sm); color: var(--color-muted); }
.form input[type="text"],
.form input[type="email"],
.form input[type="url"],
.form select,
.form textarea {
  width: 100%;
  font: inherit;
  font-size: var(--text-base);
  color: var(--color-ink);
  background: var(--color-paper);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
  padding: 0.75rem 0.9rem;
  min-height: 3rem;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.form textarea { min-height: 9rem; resize: vertical; line-height: 1.5; }
.form input::placeholder, .form textarea::placeholder { color: var(--color-muted); opacity: 0.8; }
.form input:hover, .form select:hover, .form textarea:hover { border-color: var(--color-rule-2); }
.form input:focus-visible,
.form select:focus-visible,
.form textarea:focus-visible {
  outline: none;
  border-color: var(--color-accent-deep);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-accent-deep) 30%, transparent);
}
.form__submit { margin-top: var(--space-lg); }
.form__submit .btn { width: 100%; border-radius: var(--radius-md); padding: 0.85rem 1.25rem; }
.form__legal {
  margin-top: var(--space-md);
  font-size: var(--text-sm);
  color: var(--color-muted);
}
@media (max-width: 40rem) {
  .form { padding: var(--space-md); border-radius: var(--radius-md); }
}
@media (prefers-reduced-motion: reduce) {
  .form input, .form select, .form textarea { transition: none; }
}

/* ══ Refresh (2026-09-26) ═══════════════════════════════════════
   The clothes lane only: same palette, faces and words. What changes is
   how the page is divided. Sections were ruled off with hairlines and
   their contents framed as one ruled object; now whitespace divides the
   sections and each item is its own soft card, with a real gap between
   cards. Written as one layer at the end so the previous look is one
   deletion away. */
:root {
  --card-bg: var(--color-paper-2);
  --card-border: color-mix(in oklch, var(--color-rule) 70%, transparent);
  --card-shadow: 0 1px 2px rgb(0 0 0 / 0.10), 0 10px 30px -14px rgb(0 0 0 / 0.40);
  --section-gap: clamp(4.5rem, 9vw, 7.5rem);
}
:root[data-theme="light"] { --card-shadow: 0 1px 2px rgb(42 14 30 / 0.06), 0 10px 30px -16px rgb(42 14 30 / 0.22); }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) { --card-shadow: 0 1px 2px rgb(42 14 30 / 0.06), 0 10px 30px -16px rgb(42 14 30 / 0.22); }
}

/* Sections: space, not rules. */
.spine, .notice, .rates, .qa, .refusals, .tiers-brief {
  border-top: 0;
  padding-top: 0;
  margin-top: var(--section-gap);
}
.spine__title, .notice h2, .rates h2:not(.label), .qa h2, .pledge h2 {
  letter-spacing: -0.02em;
  line-height: 1.08;
}

/* Buttons: pills, with a little lift. */
.btn { border-radius: var(--radius-pill); padding: 0.75rem 1.4rem; }
.btn--lg { padding: 0.95rem 1.75rem; }
.btn--go { box-shadow: 0 6px 20px -8px color-mix(in oklch, var(--color-accent-deep) 70%, transparent); }
@media (hover: hover) {
  .btn:hover { transform: translateY(-1px); }
}
@media (prefers-reduced-motion: reduce) { .btn:hover { transform: none; } }
.form__submit .btn { border-radius: var(--radius-pill); }

/* The five stages and the four tiers: separate cards with a gap. */
.planks, .tiers {
  border: 0;
  border-radius: 0;
  overflow: visible;
  gap: var(--space-sm);
}
.plank, .tier,
.plank:nth-child(n), .tier:nth-child(n) {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--card-shadow);
  padding: var(--space-md);
}
.tier--hi, .tier--hi:nth-child(n) {
  border-color: color-mix(in oklch, var(--color-accent-deep) 55%, transparent);
  box-shadow: var(--card-shadow), 0 0 0 1px color-mix(in oklch, var(--color-accent-deep) 35%, transparent);
}
.plank__no {
  display: inline-grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-pill);
  background: var(--color-paper-3);
  letter-spacing: 0;
}
.plank__aside { border-left: 0; padding: var(--space-xs) var(--space-sm); border-radius: var(--radius-sm); background: var(--color-paper-3); }

/* What an audit will not tell you: one card, rows inside it. */
.notice ul {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--card-shadow);
  padding: var(--space-2xs) var(--space-md);
  max-width: 52rem;
}
.notice li { max-width: none; }
.notice li:last-child { border-bottom: 0; }

/* Questions: each one its own rounded row. */
.qa details {
  max-width: 52rem;
  border: 1px solid var(--card-border);
  border-radius: var(--radius-md);
  background: var(--card-bg);
  padding: 0 var(--space-md);
}
.qa details + details { margin-top: var(--space-xs); }
.qa summary { padding: var(--space-sm) 0; align-items: center; }
.qa summary::before {
  display: inline-grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: var(--radius-pill);
  background: var(--color-paper-3);
  line-height: 1;
}
.qa__a { padding-left: calc(1.6rem + var(--space-sm)); }

/* The pledge keeps its inverted block, with a softer, larger corner. */
.pledge { margin-top: var(--section-gap); border-radius: calc(var(--radius-lg) * 1.6); }

/* Prose pages: more air between sections, a wider rhythm. */
.prose > section + section { margin-top: var(--space-2xl); }
/* Five columns left each stage card about 180px of text. Three across
   gives them room; the fifth wraps into a second row. */
@media (min-width: 62rem) { .planks { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* The scan box joins the card family: same corner, shadow and field
   treatment as the contact form, and one full-width button on a phone. */
.scanbox {
  border-color: var(--card-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--card-shadow);
  padding: var(--space-lg);
}
.scanbox__input {
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-pill);
  padding: 0.8rem 1.1rem;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.scanbox__input:focus-visible {
  outline: none;
  border-color: var(--color-accent-deep);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-accent-deep) 30%, transparent);
}
@media (max-width: 40rem) {
  .scanbox { padding: var(--space-md); }
  .scanbox__row .btn { flex: 1 1 100%; }
}
/* The rubric document sat flush against the viewport edge: .doc is not
   inside .wrap, so it gets the same gutter and measure here. The rail
   becomes a list of rounded rows with a gap, the current one filled. */
.doc {
  max-width: var(--sheet-max);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 5vw, 4rem);
}
.doc .doc { padding-inline: 0; }
@media (min-width: 62rem) {
  .doc__rail { gap: 2px; top: calc(var(--space-sm) + 5rem); }
  .doc__rail-link, .doc__rail-all {
    border-left: 0;
    border-radius: var(--radius-sm);
    padding: var(--space-3xs) var(--space-xs);
    align-items: center;
  }
}
.doc__rail-link, .doc__rail-all { border-bottom: 0; border-radius: var(--radius-sm); align-items: center; }
.doc__rail-link:hover, .doc__rail-all:hover { background: var(--color-paper-2); }
.doc__rail-link.is-here {
  background: var(--card-bg);
  box-shadow: inset 3px 0 0 var(--color-accent-deep);
}
.doc { padding-top: var(--space-lg); }
@media (max-width: 61.99rem) {
  .doc__rail-link, .doc__rail-all {
    min-height: 2.5rem;
    padding: 0 var(--space-sm);
    border-radius: var(--radius-pill);
    background: var(--color-paper-2);
    border: 1px solid var(--card-border);
  }
  .doc__rail-link.is-here { box-shadow: none; border-color: var(--color-accent-deep); }
}
