
@layer reset, page, chrome, utilities;

:root {
  /* Form controls, scrollbars and the canvas follow the page theme (2026-09-08). Light is the
     default; the two dark blocks below flip it exactly where they flip the tokens. */
  color-scheme: light;

  /* Typography */
  --font-display: 'Zodiak', 'Zodiak-fallback', Georgia, serif;
  --font-body: 'Satoshi', 'Satoshi-fallback', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;

  /* Spacing */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-20: 5rem;
  --space-24: 6rem;
  --space-32: 8rem;

  /* Transitions */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --transition-fast: 150ms var(--ease-out);
  --transition-normal: 300ms var(--ease-out);

  /* Layout */
  --max-width: 1200px;
  --nav-height: 64px;
  /* 50ch, not 65ch. The `ch` unit is the width of the "0" glyph, which in
     Satoshi is 0.68em — far wider than the ~0.5em average lowercase character.
     65ch therefore rendered ~91 characters per line, well past the 45-75 range,
     and the T2 pass that set it assumed ch meant characters. 50ch measures ~68
     real characters at every size, because both scale with the font. */
  --measure: 50ch;

  /* T2 — one reading scale for the whole site. 2.4 shipped body copy at 16px on
     the marketing pages, 17px in posts, 15px in cards, and measures of 60/65/68/70ch
     depending on which sheet you landed in. */
  --text-meta: 13px;    /* mono labels, captions, receipts */
  --text-small: 15px;   /* cards, footers, secondary lines */
  --text-body: 17px;    /* every reading surface */
  --text-lede: 19px;

  /* Radii */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  --radius-pill: 100px;
}

/* ---- Dark (default, 2026-09-09) -----------------------------------------
   Dark is the site's default: it is what a reader gets with no stored choice
   and with JavaScript off. The reader's OS preference is deliberately NOT
   consulted — light is an explicit opt-in, taken only from the toggle and
   remembered in localStorage['bw-theme'] by js/theme.js, which stamps
   data-theme before first paint so there is no flash.
   -------------------------------------------------------------------------- */
:root {
  color-scheme: dark;
  --bg: #141311;
  --surface: #1c1a17;
  --surface-offset: #242220;
  --text: #e8e2d8;
  --text-muted: #9e958a;
  --accent: #d4940a;
  --accent-hover: #e8a520;
  --accent-link: #d4940a;   /* 7.11:1 on --bg */
  --status-pass: #5fa85f;   /* 6.41:1 */
  --status-fail: #e0785c;   /* 6.20:1 */
  --border: #3a3630;
  --code-bg: #1a1816;
  --glow: rgba(212, 148, 10, 0.08);
  --glow-strong: rgba(212, 148, 10, 0.15);
  --shadow-terminal: 0 0 40px var(--glow), 0 0 80px var(--glow);
  --shadow-report: 0 0 60px var(--glow-strong), 0 0 120px var(--glow), 0 4px 30px rgba(0,0,0,0.15);
  --shadow-card: 0 0 40px var(--glow);
  --emph-border: var(--border);
  --emph-border-strong: var(--border);
  --report-rule-opacity: 1;
}

/* ---- Light: the reader asked for it ------------------------------------- */
[data-theme="light"] {
  /* LIGHT REPAIR 1 — surfaces that separate.
     Was bg #f5f0e8 → surface #f9f5ee: a four-step nudge at the top of the
     luminance range, where the eye is least sensitive, so a card existed only
     by its hairline (1.04:1 against the page). The page now sits deeper and
     cards stay near-white, so the metaphor holds — card = paper, raised —
     and the step is visible at 1.14:1. */
  --bg: #efe8db;
  --surface: #faf7f1;
  --surface-offset: #e4dbc9;
  --text: #2c2a25;
  --text-muted: #635d58;   /* was #6b6560 — 4.80:1 on the new --bg */
  --accent: #b8860b;
  --accent-hover: #9a7009;
  --accent-link: #785708;   /* WCAG AA 5.85:1 on --bg — link text only */
  --status-pass: #3a713a;   /* was #3f7a3f — 4.78:1 on the new --bg */
  --status-fail: #a8371c;   /* WCAG AA 5.73:1 on --bg */
  --border: #d3c8b3;
  --code-bg: #e9e2d3;

  /* LIGHT REPAIR 2 — subtractive shadow instead of additive glow.
     Amber at 6–12% over near-black reads as emission; over stone it is a
     smudge. A warm brown shadow with a real offset does the same job in the
     other direction, and an amber hairline keeps the accent marking the
     element as important without asking cream to emit light. */
  --glow: rgba(74, 56, 20, 0.05);
  --glow-strong: rgba(74, 56, 20, 0.08);
  --shadow-terminal: 0 1px 2px rgba(74,56,20,.05), 0 6px 20px rgba(74,56,20,.07);
  --shadow-report:   0 2px 4px rgba(74,56,20,.05), 0 10px 32px rgba(74,56,20,.09);
  --shadow-card:     0 2px 4px rgba(74,56,20,.04), 0 8px 24px rgba(74,56,20,.07);
  --emph-border:        color-mix(in srgb, var(--accent) 22%, var(--border));
  --emph-border-strong: color-mix(in srgb, var(--accent) 38%, var(--border));
  --report-rule-opacity: 0.55;
  color-scheme: light;
}

/* ---- Dark: explicit reader choice (js/theme.js sets this pre-paint) ----- */
[data-theme="dark"] {
  color-scheme: dark;
  --bg: #141311;
  --surface: #1c1a17;
  --surface-offset: #242220;
  --text: #e8e2d8;
  --text-muted: #9e958a;
  --accent: #d4940a;
  --accent-hover: #e8a520;
  --accent-link: #d4940a;
  --status-pass: #5fa85f;
  --status-fail: #e0785c;
  --border: #3a3630;
  --code-bg: #1a1816;
  --glow: rgba(212, 148, 10, 0.08);
  --glow-strong: rgba(212, 148, 10, 0.15);
  --shadow-terminal: 0 0 40px var(--glow), 0 0 80px var(--glow);
  --shadow-report: 0 0 60px var(--glow-strong), 0 0 120px var(--glow), 0 4px 30px rgba(0,0,0,0.15);
  --shadow-card: 0 0 40px var(--glow);
  --emph-border: var(--border);
  --emph-border-strong: var(--border);
  --report-rule-opacity: 1;
  color-scheme: dark;
}

/* ---- Metric-matched fallbacks so the webfont swap does not reflow ------- */
@font-face {
  font-family: 'Zodiak-fallback';
  src: local('Georgia'), local('Times New Roman');
  size-adjust: 96%;
  ascent-override: 92%;
  descent-override: 22%;
}
@font-face {
  font-family: 'Satoshi-fallback';
  src: local('Helvetica Neue'), local('Arial');
  size-adjust: 99%;
  ascent-override: 96%;
  descent-override: 24%;
}

/* ---- Site-wide primitives that every page needs ------------------------ */
@layer reset {
  /* Grayscale antialiasing everywhere. On dark grounds, subpixel rendering (Chrome/Safari default
     on macOS) thickens light-on-dark type by about half a weight step, so the same Satoshi 400
     reads heavier in one browser than another. Forcing grayscale makes every engine draw the
     lighter form — the one the design was set in. (user, 2026-09-08) */
  html {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
    /* iOS Safari inflates text when a phone is turned to landscape. Eight of the
       eleven page sheets set this in their own reset; 404 / thank-you / security
       load none of those, so those pages scaled up on rotation while the rest did
       not. Declared once here, where every page reaches it. (2026-09-08) */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
  }
  /* Zodiak ships 400 and 700 only, and every heading asks for 500. fonts.css declares the 400 file
     as the RANGE "400 500" so 500 resolves to it instead of falling through to BOLD — but the file
     is static, with no wght axis, so an engine that honours the request can still SYNTHESISE the
     missing 250 units of weight. Safari/macOS does; Chrome on Windows does not, which is exactly the
     platform split the user saw on "Verify your self-custody setup." Forbidding synthesis makes every
     engine draw Zodiak's real outlines. Scoped to the display face: body and mono ship every weight
     they ask for, and forbidding synthesis globally would silently thin a <strong> somewhere.
     (user report, 2026-09-08) */
  h1, h2, h3, h4, .nav-brand {
    font-synthesis: none;
    -webkit-font-synthesis: none;
  }

  /* Form controls do not inherit typography: the UA's "font:" shorthand gives every
     button and input its own Arial/13.33px/line-height-normal, and drops the inherited
     text-rendering. That is how "Product" in the nav came to draw without kerning or
     ligatures next to six identical links — and the same was true of "Join the Waitlist",
     "Get the checklist", "Try again", both search inputs and the icon buttons.
     Family and text-rendering only: font-SIZE is deliberately left alone, because several
     controls size themselves and inheriting 17px would resize them, and line-height is
     matched per component where a control sits beside non-control siblings (see the nav).
     (user report, 2026-09-08) */
  button, input, select, textarea, summary {
    font-family: inherit;
    text-rendering: inherit;
  }

  /* Readable measure for every paragraph and list item inside a section.
     2.3 fixed this only on .prose, so five homepage sections ran body copy
     to the full 1200px container (150+ characters per line on a 27").
     Opt out with .u-full. */
  section :is(p, li):not(.u-full, .fit-legend, .footer-tagline, .power-tools-grid span) {
    max-width: var(--measure);
    text-wrap: pretty;
  }
  h1, h2, h3 { text-wrap: balance; }
}

/* Display headings ask for the weight Zodiak actually HAS. Every page sheet sets
   font-weight: 500 on h1/h2/h3 (the design system's display weight), but Zodiak ships
   no Medium — so each engine resolved 500 its own way, and Safari drew a different face
   than Chrome while Satoshi and JetBrains Mono were identical. 400 is the file that was
   already rendering wherever it worked, so this changes nothing visually and removes the
   resolution step entirely. In @layer utilities, declared last, so it beats the page
   sheets' @layer page without !important. (2026-09-08) */
@layer utilities {
  h1, h2, h3, h4, .nav-brand { font-weight: 400; }

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

/* ==========================================================================
   Footer spacing — one authoritative copy.

   All ten page sheets declare their own .footer block, identically, in
   @layer page. Rather than patch ten files, this sits in @layer chrome, which
   the layer order at the top of this file ranks above page — so these values
   win everywhere and the footer is guaranteed identical on all 74 pages.

   The ten page-sheet copies are now dead. They are left in place rather than
   deleted blind; folding them out is a separate, verifiable pass.
   ========================================================================== */
@layer chrome {
  .footer { padding: var(--space-8) 0 var(--space-6); }
  .footer-grid {
    gap: var(--space-4) var(--space-8);
    margin-bottom: var(--space-6);
  }
  .footer-col h2 { margin-bottom: var(--space-3); }
  .footer-col ul { gap: var(--space-1); }
  .footer-bottom {
    padding-top: var(--space-4);
    gap: var(--space-1);
  }
  @media (max-width: 768px) {
    .footer-grid { gap: var(--space-4); }
  }
}

/* ==========================================================================
   T3 — prose link underlines (WCAG 1.4.1: colour is not the only affordance)
   --------------------------------------------------------------------------
   This lived in @layer reset, which every page sheet's bare
   `a { text-decoration: none; }` in @layer page outranked — so only 6 of 20
   prose links on the homepage were actually underlined. It belongs in @layer
   chrome, which sits above page, alongside the footer rules for the same
   reason: these are site-wide guarantees, not page styling.
   ========================================================================== */
@layer chrome {
  /* Two shapes of container, and they need different combinators:
     block wrappers that CONTAIN paragraphs (.blog-post, .prose …) need a
     descendant step, elements that ARE the paragraph (.report-context,
     .fine-print …) need the child step. The first version used `> a` for both,
     so every wrapper in the list silently missed — the homepage only passed
     because `section p > a` caught it, and blog posts, which are <article>
     with no <section>, matched neither branch. */
  :is(.blog-post, .prose, .faq-answer, .post-footer, .disclosure, .setup-note,
      .post-capture, .ty-next, .checklist-page) :is(p, li, dd) > a,
  :is(.report-context, .hardware-note, .hardware-small, .fine-print, .form-consent, li) > a,
  section :is(p, li) > a {
    text-decoration: underline;
    text-underline-offset: 0.18em;
    text-decoration-thickness: 1px;
    text-decoration-color: var(--accent);
  }

  :is(.blog-post, .prose, .faq-answer, .post-footer, .disclosure, .setup-note) :is(p, li, dd) > a:hover,
  :is(.report-context, .hardware-note, .hardware-small, .fine-print, .form-consent, li) > a:hover,
  section :is(p, li) > a:hover { text-decoration-color: var(--accent-hover); }

  /* chrome, cards and buttons stay bare — an underline there is noise */
  :is(.nav-links, .mobile-menu, .footer, .router-card, .blog-card, .ty-links, .link-list,
       .section-rail, .page-index) a,
  a:is(.btn-primary, .btn-secondary, .blog-card, .router-card, .link-arrow, .hero-secondary-link,
       .back-link, .back-to-blog, .post-cat, .claim-ref, .nav-brand, .skip-link) {
    text-decoration: none;
  }

  /* An in-page index lives inside .prose but is navigation, so it takes the
     bare treatment. Stated as a descendant of the prose container because the
     underline rule above is (0,2,0) — :is() takes the specificity of its most
     specific argument — and a (0,1,0) class rule cannot outrank it. */
  :is(.blog-post, .prose, .checklist-page) .page-index :is(li) > a {
    text-decoration: none;
  }
  :is(.blog-post, .prose, .checklist-page) .page-index :is(li) > a:hover,
  :is(.blog-post, .prose, .checklist-page) .page-index :is(li) > a:focus-visible {
    text-decoration: underline;
    text-underline-offset: 0.18em;
    text-decoration-thickness: 1px;
    text-decoration-color: var(--accent);
  }
}

/* ==========================================================================
   Emphasis elements read their shadow from a token, so the theme carries the
   difference rather than a [data-theme] selector. That matters: light mode is
   also the *default* (no attribute set), so a `[data-theme="light"]` rule
   would miss every reader who never touched the toggle.

   @layer chrome outranks @layer page, so these win over the base rules in
   home.css and blog.css without !important.
   ========================================================================== */
@layer chrome {
  .terminal    { box-shadow: var(--shadow-terminal); border: 1px solid var(--emph-border); }
  .report-card { box-shadow: var(--shadow-report);   border: 1px solid var(--emph-border-strong); }
  .report-card::before { opacity: var(--report-rule-opacity); }
  .post-capture, .checklist-cta { box-shadow: var(--shadow-card); }
}


@layer chrome {
  /* in chrome, not page: security.html loads confidence-report.css and would
     otherwise render the layer table unstyled. One copy, all 74 pages. */
  .proof-single,
  .proof-pair,
  .hero-shot { margin: var(--space-8) 0 0; }

  .proof-single img,
  .proof-pair img,
  .proof-strip img,
  .hero-shot img {
    display: block;
    width: 100%;
    height: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: #0a0908;
  }

  .hero-shot { margin: 0; }

  .proof-caption {
    font-family: var(--font-mono);
    font-size: var(--text-meta);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--accent-link);
    margin-bottom: var(--space-3);
  }

  .proof-note {
    font-size: var(--text-small);
    line-height: 1.6;
    color: var(--text-muted);
    margin: var(--space-3) 0 0;
    max-width: 52ch;
  }

  .proof-verdict-pass,
  .proof-verdict-fail {
    font-family: var(--font-mono);
    font-size: var(--text-meta);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    margin-right: var(--space-2);
  }
  .proof-verdict-pass { color: var(--status-pass); }
  .proof-verdict-fail { color: var(--status-fail); }

  /* pass/fail side by side — the pair is the argument, not either half */
  .proof-pair-grid,
  .proof-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: var(--space-6);
    margin: var(--space-8) 0;
  }
  .proof-item { min-width: 0; margin: 0; }

  .proof-pair-lede {
    font-size: var(--text-body);
    color: var(--text);
    margin: var(--space-4) 0 0;
  }

  /* the four air-gap layers (/security) */
  .layer-table {
    width: 100%;
    border-collapse: collapse;
    margin: var(--space-6) 0;
    font-size: var(--text-small);
  }
  .layer-table th,
  .layer-table td {
    text-align: left;
    vertical-align: top;
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border);
  }
  .layer-table thead th {
    font-family: var(--font-mono);
    font-size: var(--text-meta);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    font-weight: 500;
    background: var(--surface-offset);
  }
  .layer-table th[scope="row"] {
    font-family: var(--font-mono);
    font-size: var(--text-meta);
    letter-spacing: 0.04em;
    color: var(--accent-link);
    font-weight: 500;
    white-space: nowrap;
  }
  .layer-table code { font-size: 0.92em; }

  @media (max-width: 719px) {
    .layer-table, .layer-table thead, .layer-table tbody,
    .layer-table tr, .layer-table th, .layer-table td { display: block; }
    .layer-table thead { display: none; }
    .layer-table tr { border-bottom: 1px solid var(--border); padding: var(--space-3) 0; }
    .layer-table th, .layer-table td { border: none; padding: var(--space-1) 0; }
  }
}

@layer chrome {
  /* .see-it centres its container; a proof caption is reading copy, not a CTA */
  .proof-single, .proof-pair, .proof-strip, .proof-item,
  .proof-caption, .proof-note, .proof-pair-lede { text-align: left; }
}

@layer chrome {
  /* A terminal capture downscaled to 0.5 renders its mono at ~7px — below the
     13px floor T5 established, and worse than upscaling: the section argues
     "here is the proof" and then renders the proof illegibly. Wide, short
     captures need the full column, never half of one. */
  .proof-item img,
  .proof-single img { max-width: 100%; width: auto; height: auto; }
  .proof-item img { image-rendering: -webkit-optimize-contrast; }
}

/* ==========================================================================
   Terminal mockups — the F-10 TUI, available site-wide
   --------------------------------------------------------------------------
   Was scoped to preview.css. The homepage, /air-gap-checklist and /security all
   show station screens now, so the system lives here in @layer chrome: one copy,
   74 pages, and it outranks any page sheet that styles the same names.

   Rendered as markup rather than screenshots so the type scales with the reader,
   stays selectable and searchable, and follows the theme. The palette is fixed
   in both themes on purpose — a terminal is a terminal.
   ========================================================================== */
@layer chrome {
  .tui-shell {
    --amber:#d4940a; --amber-bright:#e8a317; --warn:#d9663a;
    --green:#5a9a5a; --cream:#e8e2d8; --gray:#9e958a; --dim:rgba(158,149,138,0.55);
    --tbg:#0a0908; --panel:#141311; --bar:#1c1a17; --tborder:#3a3630; --chip:#2a2724;
    background: var(--tbg);
    padding: 12px;
    border-radius: 14px;
    box-shadow: 0 0 0 1px rgba(0,0,0,.4), 0 0 50px rgba(184,134,11,.12), 0 14px 30px rgba(0,0,0,.14);
    margin: 0;
  }
  .tui-shell .screen {
    background: var(--panel);
    border: 1px solid var(--tborder);
    border-radius: 7px;
    overflow: hidden;
  }
  .tui-shell .statusbar,
  .tui-shell .footerbar {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-3); flex-wrap: wrap;
    background: var(--bar);
    padding: 7px 12px;
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .02em;
    color: var(--gray);
  }
  .tui-shell .statusbar { border-bottom: 1px solid var(--tborder); }
  .tui-shell .footerbar { border-top: 1px solid var(--tborder); }
  .tui-shell .body {
    font-family: var(--font-mono);
    color: var(--gray);
    padding: 16px 18px;
    font-size: 12px;
    line-height: 1.62;
    white-space: pre;
    overflow-x: auto;
    margin: 0;
  }
  .tui-shell .a    { color: var(--amber); }
  .tui-shell .ab   { color: var(--amber-bright); }
  .tui-shell .c    { color: var(--cream); }
  .tui-shell .cb   { color: var(--cream); }
  .tui-shell .g    { color: var(--green); }
  .tui-shell .m    { color: var(--gray); }
  .tui-shell .d    { color: var(--dim); }
  .tui-shell .warn { color: var(--warn); }
  .tui-shell .chip {
    color: var(--cream); background: var(--chip);
    padding: 1px 5px; border-radius: 3px; font-size: 10px;
  }

  /* a wide capture in a narrow column: shrink the grid, never the legibility */
  @media (max-width: 900px) {
    .tui-shell .body { font-size: 11px; padding: 14px; }
  }
}

@layer chrome {
  /* A watch-only extended public key is 111 characters with no break
     opportunity, so `white-space: pre` forced a horizontal scrollbar on any
     container narrower than ~950px. The session blocks wrap instead: a reader
     copying a zpub should get the whole string, not the part that fitted. */
  #opensource .code-block pre,
  .specs-section .code-block pre { white-space: pre-wrap; overflow-wrap: anywhere; }
}

@layer chrome {
  /* One treatment for every trailing note. .fine-print had it; .fit-note had no
     rule at all and was inheriting body styling by accident — two notes doing
     the same job, one styled and one not. */
  .fit-note, .fine-print {
    font-size: var(--text-meta);
    line-height: 1.65;
    color: var(--text-muted);
    /* full container width on purpose: these sit under a card grid or table and
       align with its edge. At the reading measure they read as an orphaned
       column rather than as a footnote to the block above. */
    max-width: none;
    margin: var(--space-6) 0 0;
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
    text-align: left;
  }
  /* Section ledes: same role, same size and colour, so the same measure.
     .setup-lede was 46ch and .inside .shift-paragraph 52ch. */
  .setup-lede, .inside .shift-paragraph { max-width: var(--measure); }
}

@layer chrome {
  /* The frame is drawn with U+2500/U+2502. If the JetBrains Mono subset lacks
     them the browser falls back per-glyph, and a fallback with a different
     advance width makes the right border land at a different x on every line.
     ui-monospace and the named desktop faces all carry box-drawing at the same
     0.6em advance, so the frame stays square whichever one is used.
     tabular-nums keeps the digit columns from drifting for the same reason. */
  .tui-shell pre, .tui-shell .body {
    /* No synthesised bold: the project ships JetBrains Mono 400 and 500 only,
       and a fabricated 600 is wider than the real face — enough to walk the
       right-hand border off true even with every line at exactly 49 chars. */
    font-synthesis: none;
    font-weight: 400;
    font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas,
                 'DejaVu Sans Mono', monospace;
    font-variant-numeric: tabular-nums;
    font-feature-settings: 'liga' 0, 'calt' 0;
  }
}

@layer chrome {
  /* The nav partial wraps .nav in <header role="banner">. A sticky element is
     constrained to its parent's box — and that header is exactly nav-height
     tall, so .nav had nowhere to stick and scrolled away on every page. The
     landmark carries the stickiness; .nav stays static inside it. */
  header[role="banner"] {
    position: sticky;
    top: 0;
    z-index: 100;
  }
  header[role="banner"] .nav { position: static; }

  /* Sticky needs no clipping ancestor. */
  html, body { overflow-x: clip; }
}

@layer chrome {
  /* The nav wrapped instead of overflowing: .nav-brand and .nav-cta both
     inherited white-space: normal with flex-shrink: 1, so at a narrow desktop
     width the brand broke onto two lines and the CTA rendered 69px tall inside
     a 64px band — breaking out of the nav while scrollWidth stayed equal to
     clientWidth, which is why every overflow assertion passed.
     nowrap turns a silent wrap into a visible overflow that a check can see. */
  .nav-brand, .nav-cta, .nav-dropdown-trigger { white-space: nowrap; }
  .nav-cta { flex-shrink: 0; }
}

@layer page {
  .power-tools-label, .tech-card-label, .code-block-header,
  .fit-legend, .arch-node-label, .workflow-number,
  .blog-card-cat, .blog-card-meta, .post-meta, .post-cat, .chip-n,
  .terminal-title, .footer-col h2 { font-size: var(--text-meta); }
  thead th, th[scope="col"],
  .arch-node-desc, .shot-placeholder, .shipping-badge,
  .form-consent, .form-consent a, .fine-print, .fine-print a,
  .report-card figcaption, figcaption, .faq-answer h3, .disclosure h3,
  .footer-bottom p, .footer-tagline { font-size: var(--text-meta); }
}

/* T5 floor, in @layer utilities so container rules cannot beat it. tokens.css
   declares utilities after page; layer order beats specificity. Terminal type
   stays exempt by construction — nothing here targets .tui / .report-pre. */
@layer utilities {
  thead th, th[scope="col"], .isnt-col h3, .arch-node-desc, .shot-placeholder,
  .shipping-badge,
  .tui-back, .tui-hint, .entry-meta, .chip-n,
  .annotation-label, .fit-note,
  .form-consent, .form-consent a, .fine-print, .fine-print a,
  figcaption, .faq-answer h3, .disclosure h3, .report-card h3,
  .footer-bottom p, .footer-tagline { font-size: var(--text-meta); }

  /* AUDIT 07 — the T5 pass listed ~30 selector names and then closed the list
     with a rule that only set letter-spacing, so every one of them got the
     tracking of a 13px label while keeping its authored 10–12px size. The
     names below were all authored under the floor and were never raised.
     They belong in @layer utilities, not @layer page: tokens.css loads before
     every page sheet, so a page-layer floor here loses to the page sheet's own
     11px declaration on source order. */
  .tier-badge, .post-tier-badge, .blog-chip, .blog-more, .post-list-meta,
  .related-cat, .post-cat, .blog-card-cat, .blog-card-meta, .post-meta,
  .power-tools-label, .tech-card-label, .code-block-header, .terminal-title,
  .workflow-number, .arch-node-label, .footer-col h2, .act-break .act-num,
  .section-rail a > span, .fit-legend,
  .ty-label, .ty-next h2, .ty-more h2, .ty-steps .n,
  .glossary-search-label, .glossary-count,
  .check-howto .check-label, .check-howto .check-fail-desc code {
    font-size: var(--text-meta);
  }

  /* The canonical .section-label margin-bottom moved into @layer utilities
     (utilities.css) when the eight page-sheet blocks were deleted. These three
     scoped rules tighten it, and they were declared in @layer page — which the
     move silently outranked, so all three stopped applying. Same layer, higher
     specificity: they are reached again. */
  .preview-cta .section-label,
  .triad-plinth .section-label,
  .post-capture .section-label { margin-bottom: var(--space-2); }

  /* AUDIT 08 — the floor was written to keep uppercase mono COLUMN headers
     legible, and named `th` with no qualifier. Both homepage tables use
     <th scope="row"> for their row labels, so the longest reading strings in
     each table were capped at 13px — and because this sits in @layer
     utilities, every attempt to fix it in @layer page (.fit-table
     th[scope="row"] { font-size: 15px }, still in home.css) silently lost.
     A row label is reading copy. It gets the body scale. */
  th[scope="row"] { font-size: var(--text-small); }
}

/* ==========================================================================
   AUDIT T9 — one treatment per text role
   --------------------------------------------------------------------------
   Fifteen classes were doing the work of four roles, at seven sizes, in three
   colours, at three measures, in two alignments. The worst case:
   .report-context had been swept into the 13px mono floor above, so the
   Confidence Report — the site's central artifact — introduced itself at
   caption size while Guided workflows introduced itself at 19px.

     Section lede      19px / --text       / --measure   explains a section
     Prose             17px / --text       / --measure   unchanged
     Instruction line  15px / --text-muted / its block   introduces a block
     Footnote          13px / --text-muted / full        footnotes a block

   In @layer chrome, above page, so the ten page sheets keep working while the
   markup catches up. Centring stays a property of the three CTA blocks; the
   type no longer changes with it.
   ========================================================================== */
@layer chrome {
  .section-lede,
  .shift-paragraph, .inside .shift-paragraph, .setup-lede, .report-context,
  .fit-caption, .waitlist-text, .bottom-cta-subhead, .lede, .specs-lede,
  .preview-lead, .preview-caption,
  .report-rendition-note,
  /* :not() — the first child of .see-it's container is the section label, which
     keeps its own 13px mono treatment from @layer utilities */
  .see-it > .container > p:not(.section-label) {
    font-size: var(--text-lede);
    line-height: 1.55;
    color: var(--text);
    font-style: normal;        /* .fit-caption was the site's only italic lede */
    max-width: var(--measure);
    /* The role owns these too, because two ledes had been set as mono captions:
       .preview-caption was 12px JetBrains Mono with 0.07em tracking from the
       T5 list, under a 40px h2, on a page whose own .preview-lead was correct. */
    font-family: var(--font-body);
    letter-spacing: normal;
    /* Justified, with hyphenation — at 755px an unhyphenated justify opens
       rivers, and the browser cannot break a long word without it. */
    text-align: justify;
    hyphens: auto;
    /* Without a limit the browser breaks after two characters — the first render
       gave "terminal in-\nterface". Ten characters minimum in the word, four
       before the hyphen and four after — see the prose rule below for why the
       floor is this high. */
    hyphenate-limit-chars: 10 4 4;
    -webkit-hyphenate-limit-before: 4;
    -webkit-hyphenate-limit-after: 4;
  }
  /* Scoped to the sections that OWN the centring, not to the class name.
     /air-gap-checklist reuses .bottom-cta-subhead as a left-aligned page lede,
     and home.css has two @layer page rules written to left-align it there —
     both of which a chrome rule matching on the bare class silently outranked,
     indenting that page's opening paragraph by 114px. Which is the exact defect
     class this whole audit exists to remove. */
  .waitlist-section .waitlist-text,
  .bottom-cta .bottom-cta-subhead,
  .see-it > .container > p:not(.section-label) {
    margin-left: auto;
    margin-right: auto;
  }

  .block-lead, .report-downloads-lead, .hardware-note {
    font-size: var(--text-small);
    line-height: 1.65;
    color: var(--text-muted);
    max-width: var(--measure);
  }
  /* the one instruction line that introduces a full-width row: it should end
     where the row of download pills ends, not at the reading measure */
  .report-downloads-lead { max-width: none; }

  /* .fit-note also carried .u-fine (14px, utilities, won on source order), so
     the two footnotes on the homepage were a pixel apart. .u-fine is retired. */
  .fit-note, .fine-print, .hardware-small {
    font-size: var(--text-meta);
    line-height: 1.65;
    color: var(--text-muted);
    max-width: none;
  }

  /* AUDIT 06 — a 13px uppercase mono header in --text-muted was the least
     legible type on the page. --text is the darkest neutral the system has;
     no new grey. */
  .fit-table thead th, .hardware-table thead th, .layer-table thead th,
  .spec-table thead th, .compare-table thead th { color: var(--text); }
}

/* ==========================================================================
   AUDIT — lede measure aligned to column 2
   --------------------------------------------------------------------------
   A section lede stopping at 50ch stops at nothing: measured against the
   three-column grids that sit under the ledes, 649px falls 70% of the way
   across column 2. This lands it on that column's right edge instead, so the
   text column agrees with something a reader can see.

   For a 3-track auto-fit grid with gap g in a container of content width W:
     column width c   = (W - 2g) / 3
     column-2 right   = 2c + g = (2W - g) / 3
   Percentages in calc() resolve against the containing block, which IS W here
   (.container / .opensource-content), so 200% = 2W exactly — no viewport term,
   no scrollbar drift. At the 1200px container: (2·1152 - 40) / 3 = 755px.

   Above 1000px only. Below that, auto-fit minmax(280px, 1fr) drops to two
   tracks and column 2's right edge IS the container edge, so there is nothing
   to align to and the reading measure is the better value.

   .hero-subhead is deliberately absent: the hero keeps its own 48ch.
   ========================================================================== */
@layer chrome {
  @media (min-width: 1000px) {
    .section-lede,
    .shift-paragraph, .inside .shift-paragraph, .setup-lede, .report-context,
    .fit-caption, .waitlist-text, .bottom-cta-subhead, .lede, .specs-lede,
    .preview-lead, .preview-caption,
    .report-rendition-note,
    .see-it > .container > p:not(.section-label) {
      max-width: calc((200% - var(--space-10)) / 3);
    }
  }
}

/* ==========================================================================
   AUDIT — cross-page coherence
   --------------------------------------------------------------------------
   Justification is a property of the lede role (set above). These three blocks
   are centred by their section, and centring has to win over it.
   ========================================================================== */
@layer chrome {
  .waitlist-section .waitlist-text,
  .bottom-cta .bottom-cta-subhead,
  .see-it > .container > p:not(.section-label) { text-align: center; }

  /* One page-title scale. /specs was clamp(2rem, 4vw, 3rem) = 48px and
     /air-gap-checklist clamp(2.2rem, 4.5vw, 3.4rem) = 54px, against 64px on
     every other page with the same 1200px container — three sizes for one role,
     with no rule behind which page got which.
     Blog posts keep their smaller title on purpose: an article title sits over a
     581px prose column, not a 755px lede, and 64px over that measure shouts. */
  .specs-hero h1,
  .checklist-page h1 { font-size: clamp(2.5rem, 5vw, 4rem); }

  /* Home hero: "self-custody setup." is held on one line by .u-nowrap, so the
     clamp floor has to be small enough for it to fit the narrowest column. At
     360px the container gives 312px and the phrase measures 355px at 40px, so
     the 2.5rem floor overflowed. 34px puts it at ~302px. */
  .hero-headline { font-size: clamp(2.125rem, 5vw, 4rem); }

  /* Same for section headings: /air-gap-checklist was clamp(1.5rem, 3vw, 2rem).
     Not `> .container > h2` — the heading sits inside a reveal wrapper, so the
     child combinator missed it. The success panel's own smaller headings are
     re-asserted below, since this now reaches them too. */
  .checklist-page h2 { font-size: clamp(1.75rem, 3.5vw, 2.5rem); }
  .checklist-page .checklist-success :is(h2, h3) { font-size: 1.25rem; }

  /* .specs-prose was the only prose on the site set in --text-muted, at 54ch
     rather than the measure. Body copy is --text everywhere else. */
  .specs-prose { color: var(--text); max-width: var(--measure); }
  .specs-prose strong { color: var(--text); font-weight: 500; }
}

/* ==========================================================================
   AUDIT — prose shares the lede's edge, and is justified with it
   --------------------------------------------------------------------------
   Body copy sat at the 50ch reading measure (581px at 17px) under a 755px lede,
   so every page had two different right edges stacked. One column edge per page
   now: prose ends where the lede ends. Same derivation, same 1000px gate.

   The cost is the same trade the lede made — about 100 characters a line at
   17px — so justification and hyphenation come with it rather than after it.
   ========================================================================== */
@layer chrome {
  @media (min-width: 1000px) {
    .prose, .blog-post, .specs-prose {
      max-width: calc((200% - var(--space-10)) / 3);
    }
  }
  .prose > p, .prose blockquote p,
  .blog-post > p, .blog-post blockquote p,
  .specs-prose > p,
  .post-footer > p,
  /* The record is typeset per box: each panel is its own containing block, so
     each justifies to the box it sits in rather than to the page column.
     Named by body class, not by container — the first version matched every <p>
     inside those boxes, and the labels are <p> elements, so three uppercase mono
     labels were being justified while four identical ones were not. */
  .estate-record :is(.er-plain-lede, .er-plain-body, .er-panel-body),
  .report-handoff p {
    text-align: justify;
    hyphens: auto;
    /* /security ran five hyphenated lines in a row — "name-servers", "kernel
       mod-ules", "no wire-less". hyphenate-limit-lines would be the direct fix
       and Chromium implements neither it nor its -webkit- form (CSS.supports
       returns false for both), so the run is capped indirectly: a word must be
       at least ten characters to break at all, which takes "servers" (7),
       "modules" (7) and "wireless" (8) out of the running entirely. */
    hyphenate-limit-chars: 10 4 4;
    -webkit-hyphenate-limit-before: 4;
    -webkit-hyphenate-limit-after: 4;
  }

  /* Lists, and anything mono or tabular inside the record, stay ragged. */
  .prose li, .blog-post li, .specs-prose li, .post-footer li,
  .estate-record :is(.er-block, .er-key, .er-path, .er-fields, .er-foot, .er-blank) :is(p, li, span, div),
  .estate-record :is(.er-block, .er-key, .er-path,
    .er-col-label, .er-plain-label, .er-appendix-label, .er-seal-word) {
    text-align: left;
    hyphens: manual;
  }
  /* A table cell, a caption or a definition term is not running prose. */
  .prose :is(th, td, figcaption, dt),
  .blog-post :is(th, td, figcaption, dt) { text-align: left; hyphens: manual; }
  /* Nor is a command. hyphens: auto inherited into inline code and broke the
     tokens themselves — /verify-our-claims rendered "xpubveri-fy.py". */
  :is(code, kbd, samp, pre) { hyphens: manual; }

  /* The Product column carried eleven links against three, four and three in the
     others — a four-track grid holding a list that never fitted it. auto-fit
     takes the number of tracks the content wants: six since Connect came back.
     The floor is what decides that — at 170px six tracks need 6×170 + 5×32 =
     1180px against 1152px usable, so auto-fit dropped to five and orphaned
     Legal on a second row. 150px fits six (1060px) and still clears the widest
     label, "Auditable Core". */
  .footer-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
}

/* ==========================================================================
   AUDIT — a section's padding owns the space at its edges
   --------------------------------------------------------------------------
   Measured on /confidence-report: the gap between the Estate Record and "What
   each field means" was ~180px — the record's own 48px bottom margin, plus 42px
   of section padding-bottom, plus 42px of the next section's padding-top, plus
   another 48px from that section's first child. Four rules each adding their own
   idea of the same gap.

   The section padding is the one that should decide. A first child contributes no
   top margin and a last child no bottom margin, at the container edge and one
   level in (margins collapse through a wrapper like .prose, so the wrapper's own
   first/last child has to be reached too).
   ========================================================================== */
@layer chrome {
  main > section > .container > :first-child,
  main > section > .container > :first-child > :first-child { margin-top: 0; }
  main > section > .container > :last-child,
  main > section > .container > :last-child > :last-child { margin-bottom: 0; }
}

/* ==========================================================================
   AUDIT — the record's boxes provide the measure
   --------------------------------------------------------------------------
   Measured inside one .er-plain box, whose inner width is 718px:
     .er-plain-label   390px  (a hard cap)
     .er-plain-lede    593px  (50ch at its serif size)
     .er-plain-body    512px  (50ch at 17px)
   Three right edges in one box, none of them the box's own — so justifying to
   the box could not read as justified to anything.

   A padded box IS the measure. Its children fill it, and the grid cells in
   .er-cols constrain their own children to the cell, so removing the caps gives
   one left and one right edge per box at every level.
   ========================================================================== */
@layer chrome {
  .estate-record :is(.er-plain-lede, .er-plain-body, .er-panel-body,
                     .er-col-label, .er-plain-label, .er-appendix-label,
                     .er-note p, .er-fields, .er-key, .er-path) {
    max-width: none;
  }
  /* .er-note sat narrower than the .er-panel directly above it — two boxes of
     different widths in the same stack. */
  .estate-record :is(.er-plain, .er-panel, .er-note) { max-width: none; }
}


/* ==========================================================================
   AUDIT 2.5.1 — one column edge, on /specs and /security only
   --------------------------------------------------------------------------
   The block above widened .prose, .blog-post and .specs-prose to the column
   edge and stated the intent: "prose ends where the lede ends." It never
   happened. @layer reset caps every <p> and <li> inside a <section> at
   --measure, and reset is only outranked for elements a chrome selector
   reaches DIRECTLY. Nothing in chrome re-widens those children. So the WRAPPER
   moved to 755px and the TEXT stayed at 581px: the lede and the body copy
   under it ended 174px apart, with the heading above both running to the full
   1152px container. Three right edges in one section.

   The tell was the justification. .prose > p and .specs-prose > p were given
   text-align: justify and hyphenate-limit-chars in the same pass, tuned for a
   755px column, and were applying them to a 581px one.

   Scoped to two page classes rather than fixed site-wide on purpose: the same
   defect is on the homepage, /confidence-report and 60 blog posts, and
   widening those is a separate decision with a much larger blast radius.
   Extending it later is a one-line change — add the page class to each list.

   No calc() on the children. calc((200% - var(--space-10)) / 3) is derived
   against the 1152px container; a child of .prose resolves 200% against .prose
   itself (755px) and would land at 490px. The wrapper already carries the
   edge, so the children only have to stop capping themselves.
   ========================================================================== */
/* ==========================================================================
   AUDIT — one column edge on /air-gap-checklist
   --------------------------------------------------------------------------
   Seven right edges on one page. The card column is 860px (.checklist-success
   and .checklist-expanded both cap there), but nothing else on the page knew
   that: the lede stopped at 649px, the print note at 512px, the closing copy at
   581px, and every paragraph INSIDE a card was capped by @layer reset at 50ch
   of its OWN font size — 478px of text inside an 822px box, so each card's
   explanation ended a third of the way short of the box drawn around it.

   The cards own the column. Outside them, reading copy ends where they end;
   inside them, a paragraph fills its box and the box is the measure. The lede
   also came out justified while every other string on the page is ragged-right;
   one alignment per page.
   ========================================================================== */
@layer chrome {
  .page-checklist main > section > .container > :is(h1, h2, p),
  .page-checklist main > section > .container > div > :is(h2, p),
  /* the closing block's station mock belongs to the same column: uncapped, its
     figure ran 16px past every card above it, and its caption was capped at 50ch
     of its own 13px — 462px of text under an 876px figure. */
  .page-checklist .proof-single,
  .page-checklist .proof-note { max-width: 860px; }

  /* The zpub session note reads like the provenance fine print below it, so it
     takes the full column instead of the 52ch proof-note cap. Lives here, in the
     same layer as that cap: @layer chrome outranks the page's own block. */
  /* SECTION MARKER (heading study, option C — 2026-08-10).
     A 40x2 amber rule above each section start, so a display heading on a page of
     unbroken prose reads as a new section without the type itself changing colour.
     The type keeps full --text contrast, and the amber arrives as a graphic mark
     that cannot be mistaken for a link.

     It marks the START of a section, not every h2: on the specs page that start is
     the mono .section-label, which already sits above its h2, so marking both would
     double the accent. Footer column headings are h2 as well — hence the main scope, and
     the specs hero label is excluded because it introduces the page title, not a section.
     The legal pages' hero label is excluded the same way, by the h1 that follows it. */
  .page-verify-claims main h2,
  .page-security main h2,
  .page-legal main .section-label:not(:has(+ h1)),
  .page-legal main h2:not(.section-label + h2),
  .page-specs main :not(.specs-hero) > .container > .section-label {
    position: relative;
    padding-top: var(--space-4);
  }
  .page-verify-claims main h2::before,
  .page-security main h2::before,
  .page-legal main .section-label:not(:has(+ h1))::before,
  .page-legal main h2:not(.section-label + h2)::before,
  .page-specs main :not(.specs-hero) > .container > .section-label::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 40px;
    height: 2px;
    background: var(--accent);
  }

  /* Subsection marker — security page only for now (the five boundary h3s).
     Scoped to .prose children: the timeline figure has its own h3 per slot, and a
     bare main h3 arm put amber ticks inside a graphic that never asked for them. */
  .page-security main .prose > h3 {
    position: relative;
    padding-top: var(--space-3);
  }
  .page-security main .prose > h3::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 24px;
    height: 2px;
    background: var(--accent);
  }

  #opensource .proof-note {
    max-width: none;
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
  }
  .page-checklist .bottom-cta-subhead { text-align: left; }
  .page-checklist :is(.checklist-success, .check-item, .check-howto, .check-result)
    :is(p, li) { max-width: none; }
}

@layer chrome {
  .page-specs :is(.prose, .specs-prose) :is(p, li),
  .page-security :is(.prose, .specs-prose) :is(p, li),
  .page-verify-claims :is(.prose, .specs-prose) :is(p, li) { max-width: none; }

  /* The third edge. h1–h3 were never capped anywhere on the site — the reset
     rule names p and li only — so a long heading ran to 1152px above a 755px
     lede. A heading belongs to its own text column. text-wrap: balance is
     already on from @layer reset, so the extra line breaks evenly.

     Gated at 1000px for the same reason the lede and prose rules are: below
     that the grid drops to two tracks, the calc no longer describes a column
     anything sits in, and it would cap a 600px container's headings at 386px. */
  @media (min-width: 1000px) {
    .page-specs main :is(h1, h2, h3),
    .page-security main :is(h1, h2, h3),
    .page-verify-claims main :is(h1, h2, h3) {
      max-width: calc((200% - var(--space-10)) / 3);
    }
    /* A heading inside a prose wrapper resolves 200% against the wrapper, not
       the container — same trap as the paragraphs. It fills the 755px it is
       already inside. Higher specificity (0,2,1) than the rule above (0,1,2). */
    .page-specs :is(.prose, .specs-prose) :is(h1, h2, h3),
    .page-security :is(.prose, .specs-prose) :is(h1, h2, h3),
    .page-verify-claims :is(.prose, .specs-prose) :is(h1, h2, h3) { max-width: none; }
  }
}

/* ==========================================================================
   Nav lockup (2026-09-09) — the brand mark, drawn rather than loaded.

   The arrangement the Brand folder uses in bw-header-safe-triad: an amber rule,
   the wordmark in the display face, and the triad beneath it. Drawn rather than
   shipped as a PNG so the wordmark stays real text, it costs no bytes, it stays
   crisp at any zoom, and it follows the theme by itself.

   (The alpha-channel exports of the same artwork live in Brand/export as
   bw-header-safe-triad-alpha-{stone,ink}.png if this is ever revisited.)

   Sized in em against .nav-brand's own font-size, so the smaller nav on
   thank-you.css scales with it instead of needing its own rule. Unlayered on
   purpose: ten page stylesheets carry their own .nav-brand block inside
   @layer page, and an unlayered rule outranks every one of them without
   !important and without editing ten files.
   ========================================================================== */
.nav-rule {
  flex-shrink: 0;
  width: 3px;
  height: 1.9em;
  border-radius: 1px;
  background: var(--accent);
}
.nav-lockup {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
}
.nav-wordmark {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.15;
}
/* The strap is amber on ink and muted on stone, matching the two exports. */
.nav-strap {
  font-family: var(--font-mono);
  font-size: 0.56em;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1;
  margin-top: 3px;
  white-space: nowrap;
  color: var(--accent);
}
[data-theme="light"] .nav-strap { color: var(--text-muted); }

/* Narrow viewports: the triad is the first thing to go, and the rule shortens to
   match a single line of wordmark. */
@media (max-width: 720px) {
  .nav-strap { display: none; }
  .nav-rule  { height: 1.25em; }
}
