/* 🎨 sanctuary.css — the shared design system (design-review D1). ONE palette,
   ONE type ramp, ONE 8px space scale, ONE button. Extracted from the dominant
   theme (season/wiki/the games). Pages opt in by linking this; adopt the drifting
   legacy pages incrementally. (welcome.html's warm-paper hero is a deliberate
   exception — the front door.)

   ✨ THE PRESTIGE PASS: an obsidian-and-champagne, editorial-luxury treatment
   (the Robb Report register) laid over the Sanctuary's violet identity — gold is
   now the star, headlines are cream serif with a magazine eyebrow, geometry is
   crisper, whitespace more generous, borders hairline. Same tokens, richer feel;
   nothing structural changed, so every page inherits the lift for free.

   🔤 CN6 — THE FONT FOLD (the connector line). Type is now SELF-HOSTED, and the
   privacy line moved from "zero @font-face" to "zero EXTERNAL font origins" —
   consciously, in the same change that amended docs/audits/offline-privacy-audit.md.
   Every src below is a same-origin absolute path under /fonts/; there is no CDN,
   no Google Fonts, no `url(http…)`, no @import, anywhere in this file or its twins.

   The editorial serif is Anthropic Serif — keeper-cleared to serve 2026-08-06 and
   re-affirmed by Nick 2026-08-18, so the Source Serif 4 stand-in is gone. Only the
   three subsetted woff2 faces below are served (400, 400-italic, 600 — subset from
   the Text family). The 24 OTF masters stay OUTSIDE this tree at
   art/fonts/anthropic-serif/ and are never copied in or served raw: web faces ship
   as woff2 only. IBM Plex Mono stays the OFL terminal mono.

   DEGRADES GRACEFULLY: if a .woff2 is absent the browser 404s the face and falls
   straight through to the next family in the stack (the pre-CN6 stack, unchanged),
   so a missing binary costs looks, never function. See fonts/README.md for the
   exact five files this expects. font-display:swap so text is never invisible. */
@font-face{ font-family:'Anthropic Serif'; font-style:normal; font-weight:400; font-display:swap;
  src:url('/fonts/anthropic-serif-400.woff2') format('woff2'); }
@font-face{ font-family:'Anthropic Serif'; font-style:italic; font-weight:400; font-display:swap;
  src:url('/fonts/anthropic-serif-400-italic.woff2') format('woff2'); }
@font-face{ font-family:'Anthropic Serif'; font-style:normal; font-weight:600 700; font-display:swap;
  src:url('/fonts/anthropic-serif-600.woff2') format('woff2'); }
@font-face{ font-family:'IBM Plex Mono'; font-style:normal; font-weight:400; font-display:swap;
  src:url('/fonts/ibm-plex-mono-400.woff2') format('woff2'); }
@font-face{ font-family:'IBM Plex Mono'; font-style:normal; font-weight:600 700; font-display:swap;
  src:url('/fonts/ibm-plex-mono-600.woff2') format('woff2'); }
:root{
  /* obsidian ground, a warm champagne vignette instead of a cold one */
  --bg:#08060e; --bg2:#140f22; --panel:rgba(22,15,40,.5); --panel2:rgba(16,10,30,.62);
  --violet:#b9a0ff; --violet-2:#cdbcf0; --gold:#f3d9a4; --gold-2:#caa96a; --champagne:#e8cfa0;
  --rose:#e9c2d8; --teal:#7fe3d3;
  --ink:#f4eede; --ink-dim:#cbbfe6; --ink-soft:#a99ac9; --faint:#8a7ab0;
  --edge:rgba(232,207,160,.14); --edge-2:rgba(232,207,160,.34);   /* hairline, gold-tinted */
  --hair:rgba(232,207,160,.16);
  /* a taller, more editorial ramp */
  /* CN6: the token NAME is fenced by four suites — never rename --serif. Only its
     VALUE changed: the self-hosted face leads, the historical stack stays as the
     fallback so a missing binary is invisible in behaviour. */
  --serif:'Anthropic Serif','Iowan Old Style','Palatino Linotype','Book Antiqua',Georgia,'Times New Roman',serif;
  /* CN6: the NEW token — one mono for both terminal skins and every code surface. */
  --mono:'IBM Plex Mono',ui-monospace,Consolas,Menlo,monospace;
  --t-display:clamp(30px,5.4vw,42px); --t-h:19px; --t-lead:16px; --t-body:14px; --t-cap:12px;
  --s1:8px; --s2:16px; --s3:24px; --s4:40px; --s5:64px;
  --radius:12px; --radius-pill:24px; --maxw:820px;
}
.sanctuary{ font-family:var(--serif);
  background:radial-gradient(ellipse at 50% -8%, rgba(243,217,164,.10), transparent 42%), radial-gradient(ellipse at 50% 0%, var(--bg2), var(--bg) 72%);
  color:var(--ink); -webkit-font-smoothing:antialiased; }
.sanctuary a{ color:var(--gold); text-underline-offset:3px; text-decoration-color:var(--edge-2); }
.sanctuary a:hover{ color:var(--champagne); }
.sanctuary .wrap{ max-width:var(--maxw); margin:0 auto; padding:var(--s4) var(--s2) var(--s5); }
.sanctuary .seo-intro{ max-width:760px; margin:0 auto var(--s3); padding:var(--s2) var(--s3); border:1px solid var(--edge); border-radius:var(--radius); background:rgba(177,140,255,.05); }
.sanctuary .seo-intro h1{ margin:0 0 var(--s1); font-size:clamp(24px,4vw,34px); }
.sanctuary .seo-intro p{ margin:0; color:var(--ink-soft); line-height:1.6; }
/* headlines: cream serif, tight tracking — the magazine cover, not a UI label */
.sanctuary h1{ font-size:var(--t-display); font-weight:normal; color:var(--ink); margin:0 0 var(--s2); letter-spacing:-.01em; line-height:1.14; }
.sanctuary h2{ font-size:var(--t-h); font-weight:normal; color:var(--champagne); letter-spacing:.02em; margin:var(--s3) 0 var(--s2);
  padding-bottom:6px; border-bottom:1px solid var(--hair); }
/* the eyebrow — gold, spaced, uppercase: the Robb-Report kicker */
.sanctuary .kick{ color:var(--gold); font-size:var(--t-cap); letter-spacing:.24em; text-transform:uppercase; font-family:var(--serif); }
.sanctuary .lead{ color:var(--ink-dim); font-size:var(--t-lead); line-height:1.7; max-width:62ch; }
.sanctuary .dim{ color:var(--faint); }
.sanctuary .card{ background:linear-gradient(180deg,var(--panel),var(--panel2)); border:1px solid var(--edge); border-radius:var(--radius);
  padding:var(--s3) var(--s3); box-shadow:0 1px 0 rgba(243,217,164,.05) inset, 0 18px 40px -28px rgba(0,0,0,.8); }
/* the primary action — champagne on obsidian, a spaced serif label */
.sanctuary .btn{ display:inline-block; font-family:var(--serif); font-size:var(--t-body); letter-spacing:.04em; border:1px solid var(--gold-2); border-radius:var(--radius-pill);
  padding:11px 22px; background:linear-gradient(135deg,var(--gold),var(--gold-2)); color:#1a1206; font-weight:bold; cursor:pointer; text-decoration:none; }
.sanctuary .btn:hover{ filter:brightness(1.05); }
.sanctuary .btn.ghost{ background:none; color:var(--gold); border:1px solid var(--edge-2); font-weight:normal; }
.sanctuary .btn:disabled{ opacity:.5; cursor:default; }
.sanctuary .pill{ display:inline-block; font-size:var(--t-cap); letter-spacing:.02em; padding:4px 12px; border-radius:var(--radius-pill); background:var(--panel2); border:1px solid var(--edge); color:var(--ink-soft); text-decoration:none; }
.sanctuary .pill.on{ border-color:var(--gold); color:var(--gold); }
.sanctuary .row{ display:flex; gap:var(--s1); flex-wrap:wrap; align-items:center; }
.sanctuary input, .sanctuary textarea{ font-family:var(--serif); font-size:var(--t-body); color:var(--ink); background:rgba(0,0,0,.3);
  border:1px solid var(--edge); border-radius:10px; padding:10px 13px; width:100%; }
.sanctuary input:focus, .sanctuary textarea:focus{ outline:none; border-color:var(--gold-2); box-shadow:0 0 0 3px rgba(243,217,164,.12); }
.sanctuary textarea{ min-height:90px; resize:vertical; }
.sanctuary .muted-note{ font-size:var(--t-cap); color:var(--faint); }
@media (prefers-reduced-motion: reduce){ .sanctuary *{ animation:none !important; transition:none !important; } }

/* The menu's opt-in fast display mode is reversible and intentionally honest:
   it removes motion and procedural atmosphere cost; it does not claim to
   change the network size of page assets. */
html[data-bh-fast-load="1"] * ,
html[data-bh-fast-load="1"] *::before,
html[data-bh-fast-load="1"] *::after {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  scroll-behavior: auto !important;
}
html[data-bh-fast-load="1"] .atmo-grain,
html[data-bh-fast-load="1"] .atmo-vig { display:none !important; }
.menu-preferences { display:flex; flex-wrap:wrap; gap:6px; padding:8px 10px 10px; border-top:1px solid var(--edge); }
.menu-preferences button { border:1px solid var(--edge); border-radius:999px; padding:5px 9px; background:transparent; color:var(--ink-dim); cursor:pointer; font:inherit; font-size:12px; }
.menu-preferences button:hover, .menu-preferences button:focus-visible { color:var(--ink); border-color:var(--edge-2); }

/* 🖨 ARK3 — THE PRESS. The tier below "no internet" is "no electricity": every
   token page prints as clean black-on-white paper. Chrome (nav bars, buttons,
   inputs, the radio) drops away; links reveal their destinations so a paper
   copy still points somewhere. A printed page must serve a stranger with no
   one left to explain it. */
@media print{
  .sanctuary{ background:#fff !important; color:#000 !important; font-size:12pt; }
  .sanctuary *{ background:transparent !important; color:#000 !important; box-shadow:none !important; text-shadow:none !important; }
  .sanctuary h1, .sanctuary h2{ color:#000 !important; border-color:#000 !important; page-break-after:avoid; }
  .sanctuary .card{ border:1px solid #000 !important; page-break-inside:avoid; }
  .sanctuary a{ text-decoration:underline; }
  .sanctuary a[href^="http"]::after, .sanctuary a[href^="/"]::after{ content:" (" attr(href) ")"; font-size:9pt; }
  .sanctuary .tbar, .sanctuary nav, .sanctuary .btn, .sanctuary .pill, .sanctuary button,
  .sanctuary input, .sanctuary textarea, .sanctuary video, .sanctuary canvas, .sanctuary audio,
  #moreMenu, #toast, #radio, .no-print{ display:none !important; }
  /* shell-layer chrome (see THE SHELL LAYER below) — transient UI never prints */
  .sanctuary .scrim, .sanctuary .dialog, .sanctuary .toast, .sanctuary .skel,
  .sanctuary .skip-link{ display:none !important; }
  /* …but .sheet and kbd are CONTENT: keep them, just give them ink-on-paper edges */
  .sanctuary .sheet{ border:1px solid #000 !important; page-break-inside:avoid; }
  .sanctuary kbd{ border:1px solid #000 !important; }
  .print-only{ display:block !important; }
}
.print-only{ display:none; }

/* ============================================================================
   🏗 THE SHELL LAYER (slice S1-DESIGN-SHELL) — ADDITIVE ONLY.
   Everything above this line is byte-for-byte unchanged except SIX new lines
   (two comments + three rules) inside @media print, because the 33 pages that
   link this file already depend on it. HOW TO RE-VERIFY THAT WITHOUT TRUSTING
   ME, and how it was re-verified for this pass: the pre-slice stylesheet is
   baked into the storefront image, so
     docker run --rm blackhorn-storefront cat //app/sanctuary.css
   yields the 75-line baseline, and `diff` against `head -81` of this file
   reports exactly one hunk — those six lines, at 73-78. RE-RUN AND CONFIRMED
   VERBATIM ON THIS PASS: 75 lines in, one hunk `72a73,78`, two comments and
   three rules. (Do not reach for the RUNNING container as an earlier phrasing
   here did — it is stopped, so a recipe that depends on it silently yields
   nothing. And note the DOUBLE slash: under Git Bash on Windows a bare
   `/app/sanctuary.css` is path-mangled to `C:/Program Files/Git/app/…` and the
   command fails with "can't open" — which reads like a missing file rather
   than a shell artefact. That mangling ate this recipe once on this pass.)

   WHY: the system shipped a palette, a type ramp and ONE button, and pages then
   hand-rolled everything the system did not name.

   ── RECON, RE-MEASURED ON DISK 2026-08-03 (TWO AUDIT PASSES) ────────────────
   The first cut of this block shipped five wrong counts. A second adversarial
   pass over the CORRECTED version then found eighteen more false or imprecise
   statements — in this recon block and in sections A, C, D, E, F, G and H —
   every one of them flagged in place below with what the earlier draft said.
   None of them changed what the stylesheet DOES (the one code change in the
   whole second pass is the kbd stack in section G, which had contradicted its
   own comment); all of them were doctrine other agents would have inherited.
   The blast-radius reasoning here is only as good as these numbers, so each is
   restated with the source you can re-run it against. RE-COUNT BEFORE YOU TRUST
   THEM: twenty-three wrong statements across two passes, in a block written
   carefully both times, is the honest base rate for prose of this kind. A THIRD
   adversarial pass then found eight more — six of them stale rather than
   originally false, because concurrent slices kept editing pages underneath
   this prose. That is the real lesson of this block: prose about a moving repo
   ROTS, and rot reads exactly like a lie to the next agent. Basis, re-counted
   on the third pass: the 122 .html files in apps/storefront, 36 of which link
   this file and 33 of which mount it. Where a sentence below still says "120"
   or "31" it is a stale denominator I missed — treat any bare count here as
   suspect and re-run the command beside it.
     • ADOPTION, AND THE ONLY VOLATILE NUMBERS IN THIS BLOCK. At 17:55 local on
       2026-08-03: 122 .html files, 36 link this file, 33 mount
       `<body class="sanctuary">`, and the THREE that link without mounting are
       ceremony, mainframe and write — nothing here can reach those, because
       every selector below is .sanctuary-scoped. Reproduce all four with
         ls *.html | wc -l
         grep -l 'sanctuary\.css' *.html | wc -l
         grep -l 'class="sanctuary' *.html | wc -l
         comm -23 <(grep -l 'sanctuary\.css' *.html|sort) \
                  <(grep -l 'class="sanctuary' *.html|sort)
       DO NOT TRUST THE FOUR NUMBERS ABOVE. They moved three times DURING this
       one audit — 120/33/29, then 122/34/31, then 122/36/33 — because
       concurrent slices kept converting pages onto the frozen palette while it
       ran (404.html and watch.html both flipped from not-mounting to mounting
       inside the hour; neither declares any name this file defines, so nothing
       collided). Every OTHER claim below is deliberately phrased against a
       named file and line, or as "no mounted page does X", precisely so that it
       cannot rot the way these can. When a fact here needs a denominator that
       changes hourly, that is a smell: assert the property, not the count.
     • 18 files re-invent the auto-fit card grid, at 12 different minmax
       minimums (140/150/170/200/220/230/238/240/250/260/320px and 4.5rem).
     • 13 files define a `.sheet`, and they are NOT byte-identical. Twelve of
       them mount .sanctuary; eleven of those twelve share one var()-based
       declaration, and clockwork.html:25 is the odd one out (padding:14px 16px;
       margin:12px 0; background:rgba(0,0,0,.2)). The thirteenth is
       write.html:29, which does not mount .sanctuary and means something else
       entirely by the name — a 760px page container. That is how load-bearing
       this name is.
     • 5 files carry a rule matching `.empty`: clockwork.html:43,
       grimoire.html:23 and impact-panel.html:22 declare a bare `.empty`,
       arcana.html:53 scopes it `.book .empty`, and ninebells.html:28 writes
       `#play.empty`. Only clockwork mounts .sanctuary — which is why the blast
       radius really was one page even though the count was wrong.
     • WAS "zero `.sr-only`, zero `scroll-margin`/`scroll-padding`, zero
       `.skel` in all 120, and no `<dialog>`/`role="dialog"` anywhere". Only
       part of that is still true, and the part that changed is the part that
       matters most — TWO OF THESE PRIMITIVES ARE NOW LOAD-BEARING:
         – `.sr-only` is USED, and by nothing but this file. news.html:45, :105
           and :111 emit `<span class="sr-only"> (opens in a new tab)</span>`,
           news.html MOUNTS, and a repo-wide grep for a `.sr-only` RULE finds
           exactly one — the one in section B below. Delete section B's rule and
           that parenthetical becomes visible body text on a live page.
         – `.skip-link` is SEATED. watch.html:46 is a real
           `<a class="skip-link" href="#main">Skip to the video wall</a>` with a
           real target at watch.html:48 (`<main class="wrap" id="main">`), and
           watch.html declares no skip-link CSS of its own. Delete section B's
           rule and that link renders as visible chrome at the top of the page
           instead of being parked at top:-100px.
       Still true, re-counted on the third pass over the 122: zero `.skel`, zero
       `scroll-margin`/`scroll-padding` outside this file, no `<dialog>` element
       and no `role="dialog"` anywhere.
       The rest of that sentence's first cut was stated far too broadly, so it
       is restated here at the scope where it is actually true:
         – `aria-live` was "in exactly one of the 120 pages (reflection.html:58)".
           Now FOUR of the 122 carry it — today.html:48/:52/:73,
           reflection.html:58, guild.html:59/:68/:248, guilds.html:71/:74 — plus
           wheel.js:599 at runtime. Of those, today and wheel MOUNT and
           reflection does NOT (it links nothing here), which inverts the
           earlier reading of this bullet entirely. Worth knowing for section F:
           guild.html:68 and guilds.html:74 are already
           `<div id="toast" role="status" aria-live="polite">`, so the mount
           attributes .toast needs are not hypothetical — two pages wrote them,
           and neither page mounts .sanctuary. Copy those two lines.
         – `.dialog` and `.toast` are NOT free names repo-wide: lightforge.html
           declares both (:86 and :93). TEN files declare a toast style rule
           (course, credit, guild, guilds, highstreet, lightforge, market,
           outflow, pay-crypto, site); only outflow mounts .sanctuary, and it
           spells it `#toast` (an id), so nothing collides today. See "WHAT I
           DID NOT NAME" below, which had this right while this bullet did not.
         – focus: across the 33 MOUNTED pages there are ZERO `:focus` rules of
           any kind — that is the number that matters here, and it is stronger
           than what the first cut claimed. (Said "31" until the third pass;
           re-run `grep -c ':focus' $(grep -l 'class="sanctuary' *.html)` and
           every line still ends in `:0`.) Across all 122 there are plenty, and
           they are not only on text fields (charselect.html:24, foolee.html:21
           /:51/:58, hearthpond.html:31, orrery.html:21/:28, repair.html:32/:51
           style focus on buttons, links and tiles). None of those pages mount.
   This block names those primitives once, using only the frozen tokens, so the
   next page can stop guessing.

   ── WHAT THE SUITE ACTUALLY COVERS HERE (:root YES, THE SHELL LAYER NO) ─────
   The first two passes said "two test files mention this stylesheet and neither
   tests anything below this line", and drew from it that a CSS edit cannot
   redden the suite. The second half of that is right; the first half is not,
   and the difference matters, because :root IS fenced and the shell layer is
   not. Re-counted on the third pass — FIVE test files reference this file, and
   FOUR of them read it off disk (`grep -rn "readFileSync[^)]*sanctuary" test/`):
     • test/events.test.js:521 parses the `:root` BLOCK ONLY into DECLARED_TOKENS,
       asserts `size > 25` and that --bg/--gold/--ink/--serif/--s1/--radius/--maxw
       are all present (:530-534), then checks every var() on events.html and its
       JS against it. Rename or drop a token from :root and this goes RED.
     • test/velvet-roulette.test.js:657 and test/wheel.test.js:882 do the same
       shape, but scan the WHOLE file for `--x:` rather than just :root, and
       assert `size > 20`. Same effect for a deletion; note that a custom
       property declared anywhere below would silently widen their allow-list.
     • test/topbar-shell.test.js:144 reads this file to recover the specificity
       of ONE rule — `.sanctuary .row` on line 50 — and falls back to a hardcoded
       (0,2,0) when it cannot find it (:152), so it degrades quietly instead of
       reddening. Its own token allow-list (:180-185) is a HAND TRANSCRIPTION of
       :root, not a read, and it audits topbar.js and index.html against it,
       never this file — so :root and that list can drift apart silently.
     • test/video-rank.test.js:429 asserts the LINK TAG
       `<link rel="stylesheet" href="/sanctuary.css">` against watch.html, as
       does events.test.js:556 and velvet-roulette.test.js:664. A link tag is not
       a stylesheet. (A plain `grep -rl 'sanctuary\.css' test/` MISSES this file,
       because it writes the path regex-escaped as `sanctuary\.css`. That is how
       the "two files" count was reached and why it was wrong.)
   NET: the TOKEN CONTRACT has real teeth — break :root and four suites go red.
   EVERYTHING BELOW THIS LINE HAS NONE. Delete this whole block, inop it, invert
   a rule, or point a selector at nothing, and the suite stays green, because not
   one test reads a selector or a declaration from the shell layer. A green suite
   is evidence the tokens survived. It is NOT evidence a change here is safe — a
   browser is.
   PROVENANCE, because "measured" was used loosely here and that matters: claims
   about COMPUTED VALUES (what a page renders, which rule wins in the box model)
   came from getComputedStyle in headless Chromium against the real pages during
   the first two passes. Claims about COUNTS, LINE NUMBERS and SPECIFICITY in the
   second and third audit passes did not — they came from greps over the 122
   files on disk and from a selector-weight calculation done by hand, and the
   contrast ratios
   from the WCAG relative-luminance formula, not from a rendering engine. Both
   kinds are re-runnable; neither is "I read it and it looked right", which is
   what produced every wrong statement corrected below.
   ONE TRAP IF YOU REACH FOR THAT HELPER: topbar-shell.test.js's `specificity()`
   counts `:where` and `:is` as ordinary classes, does not zero what is inside a
   `:where()`, and counts every type selector inside an `:is()` list. Point it at
   the .sheet heading reset below and it returns (0,5,6); the real weight is
   (0,1,1). It is correct for the plain selectors it was written for. Do not
   point it at this block.

   ── THE SPECIFICITY POLICY (read this before adding a rule here) ─────────────
   Every page links this file in <head> and opens its own <style> right after,
   so a page's rules always come LATER in the cascade. The goal is that the page
   always wins: weigh no more than the page does, and let source order settle it.

     • SINGLE-CLASS primitives are written `:where(.sanctuary) .thing`.
       `:where()` contributes ZERO specificity, so the whole selector weighs
       (0,1,0) — the same as a page's plain `.thing` — and the page, being
       later, WINS. Measured: today.html:10's `.grid{grid-template-columns:1fr
       1fr}` beats `:where(.sanctuary) .grid` below. today.html and call.html:16
       own a `.grid`, clockwork.html:43 owns an `.empty`, twelve pages own a
       `.sheet`; they keep exactly what they have.

     • THIS DOES NOT CARRY OVER TO ANY OTHER SHAPE, and the first cut of this
       block claimed it did ("every component/layout primitive contributes zero
       specificity"). It is true of the single-class rules ONLY. A descendant or
       child selector adds a second class-level unit and then OUT-weighs a page:
         `:where(.sanctuary) .sheet > :first-child` weighed (0,2,0), because
         `:first-child` is a pseudo-class and counts like a class.
         onepagers.html:16 declares `.sheet table{ …; margin:8px 0 }` at (0,1,1),
         and a <table> leading a .sheet on that page computed margin-bottom:8px
         (page wins) with margin-top:0px (SHELL wins). The shell silently beat
         the page on one longhand. `.dialog > :first-child`, `.dialog .foot`,
         `.menu > :hover`, `.menu > .on`, `.empty .kick`,
         `.sidebar.rail-left > .aside` and `.sidebar > .aside.sticky` all had
         the same shape, at (0,2,0) or (0,3,0).
       Section H had the same problem in a different costume: `h1[id]`, `h2[id]`,
       `h3[id]` are (0,1,1) each, type plus attribute.
       All of them now move the ancestor chain INSIDE the :where() — either
       `:where(.sanctuary .parent) .child`, or, where nothing at all needs to be
       matched on, the whole chain (`:where(.sanctuary .sidebar > .aside.sticky)`
       is (0,0,0)). Section H folds its type list into a `:where()` too, so only
       the `[id]` counts. No DESCENDANT-OR-CHILD rule in C-H is above (0,1,0) now
       except the two heading resets named next — and note that the compound
       modifiers two bullets down still are, by construction and on purpose, so
       do not read this as "nothing in C-H exceeds (0,1,0)". An earlier draft of
       this line said exactly that and contradicted its own next-but-one bullet.
       Recomputed selector by selector, with :where() zeroed and :is() scored as
       the max of its arguments: the only weights in C-H are (0,0,0), (0,0,1)
       (`kbd`), (0,1,0), (0,1,1) for the two resets, and (0,2,0)/(0,3,0) for the
       fifteen compound modifiers that sit on a primitive's OWN element
       (.cluster.between/.end/.baseline, .grid.narrow/.wide, .sidebar.rail-left,
       .scrim.on/.top/[hidden], .dialog.wide, .toast.on,
       .skel.line/.line.short/.block/.circle). THE TEST: whatever you are
       matching *on* goes outside the :where(); whatever *under* goes inside it.
       Then count, remembering that a pseudo-class weighs the same as a class
       and an attribute selector does too.

       THE AUDIT LEDGER for the four rules that carried this defect, re-checked
       one at a time against the file as it now stands, because "all of them now
       move the chain inside the :where()" is exactly the kind of sweeping claim
       this block keeps getting wrong. Each line is the selector on disk, its
       weight, and the page rule it used to be able to beat:
         – `.sheet > :first-child` — GONE, not merely lightened. It is now
           `:where(.sanctuary .sheet) > :is(h1…h6):first-child` (0,1,1), which
           cannot match a <table> or a <p> at all. onepagers.html:16's
           `.sheet table{margin:8px 0}` is out of reach by SELECTOR now, not by
           weight. See section D for why the general form was withdrawn.
         – `.dialog .foot` — now `:where(.sanctuary .dialog) .foot` (0,1,0).
           Rival: index.html:92 `.foot{max-width;margin;padding}` (0,1,0), on a
           mounted page. Ties, page is later, page wins.
         – `.menu > .on` — now `:where(.sanctuary .menu) > .on` (0,1,0), and the
           `[aria-selected="true"]` arm beside it is (0,1,0) too. No mounted page
           declares `.menu` at all, so this is precautionary rather than a live
           save.
         – `.empty .kick` — now `:where(.sanctuary .empty) .kick` (0,1,0).
           Rival: `.sanctuary .kick` on line 37 is (0,2,0) and OUT-weighs it, so
           colour/size/tracking still come from the palette and only display and
           margin-bottom come from here. That is the intended split; it is noted
           because the weights are NOT symmetric and reading this rule as
           "overrides .kick" would be wrong.
       So: nothing in that list can beat a page any more, and the two heading
       resets in the next bullet are the only remaining shell-beats-page rules.

     • TWO RULES DELIBERATELY WEIGH (0,1,1) — the only two in sections C-H that
       break the paragraph above, named here so the ledger stays honest: the
       leading-heading resets for `.sheet` and `.dialog`. `.sanctuary h2` on line
       34 is (0,1,1) and sets margin-top:var(--s3), so a heading reset must
       out-rank it or it does nothing at all, and (0,1,1) is the lightest weight
       that can. Written `:is(h1,h2,h3,h4,h5,h6):first-child` under a
       `:where()`-ed parent they are exactly that: they TIE line 34 and win on
       source order, and they tie a page's own `.sheet h2{margin-top:0}` — later
       still, so the page wins.
       THE PRICE, measured rather than glossed: they DO out-weigh a page's plain
       single-class rule on that heading. A page writing `.hd{margin-top:13px}`
       for an `<h2 class="hd">` leading a .sheet computes 0px, not 13px. The
       escape hatch is any (0,1,1)-or-heavier page selector — `.sheet h2`,
       `h2.hd`, an id — which is what a page overriding a heading writes anyway.
       This is the one place in C-H where the shell can beat a page. If that
       trade is ever judged wrong, delete both resets: the twelve `.sheet` pages
       are unaffected either way (nine already declare `.sheet h2{margin-top:0}`
       themselves) and new pages simply write it as those nine did.

     • MODIFIER COMPOUNDS on a primitive's OWN element (`.grid.narrow`,
       `.btn.ghost`, `.scrim.on`, `.skel.line.short`) weigh (0,2,0)+ by
       construction, and that is NOT the same defect: a page overriding a
       modifier writes the same compound, so the weights are symmetric and
       source order still hands the page the win. Left as they are.

     • A11y rules (focus, .sr-only, .skip-link) are written `.sanctuary .thing`
       instead, at (0,2,0)+, because they must NOT be lost to an accident. The
       focus block has to reach past `.sanctuary input:focus{outline:none}` on
       line 53, which is (0,2,1) — and note that it does not OUT-weigh that
       rule, it MATCHES it. `.sanctuary input:focus-visible` is (0,2,1) too
       (class + type + pseudo-class) and wins only by being later in the same
       file. That is the whole reason the explicit element list exists: the
       (0,2,0) catch-all above it would LOSE to line 53 and no text field would
       ever show a ring. This is the one place where beating a page is the point.

   ── WHAT I DID NOT NAME, AND WHY ─────────────────────────────────────────────
   The brief suggested `.sheet` / `.overlay` for the modal. Both names are
   already taken in the field with DIFFERENT meanings, so using them would have
   broken live pages:
     • `.sheet` is a card-ish page SECTION on the 12 mounted pages that define
       it (almanac:14, clockwork:25, gtable:14, identify:19, lookup:19,
       medic:19, memory:14, onepagers:14, school:14, spark:14, well:14,
       workshop:14) — and a 760px page CONTAINER on write.html:29, which links
       this file but does not mount. So `.sheet` is promoted below as the
       section it already is — not a modal.
     • `.overlay` is a fixed, full-bleed HUD layer on five game pages
       (foolee:13, landfall:10, openworld:10, quantumforge:14, worldgenrace:10).
       FOUR of the five also set pointer-events:none; foolee.html:13 does not,
       so "a pointer-events:none layer on five pages" was wrong. None mount
       .sanctuary today, but they will, and a backdrop under that name is a
       landmine. The backdrop is called `.scrim` instead — free as a CLASS,
       but note three non-mounting pages already spell it as an ID (foolee:11,
       quantumforge:11, repair:11). A page that mounts and copies that markup
       gets no backdrop from here: `.scrim` cannot match `<div id="scrim">`.
     • `.dialog`, `.dialog .foot` and `.toast` are all already live in
       lightforge.html:86-95 — which does NOT link this file and does NOT mount
       .sanctuary, so nothing collides today. If lightforge ever mounts, its own
       rules are later and no lighter than these, so they still win.
   The modal is therefore `.scrim` + `.dialog`, both unclaimed inside .sanctuary.

   ── WHAT CSS CANNOT DO (the mount is still a JS/HTML job) ────────────────────
   A stylesheet cannot add an attribute. For these primitives to be real:
     • .toast needs `role="status" aria-live="polite"` on the mount, or it stays
       silent to screen readers. (This said "aria-live appears ONCE in 120 pages
       today — reflection.html, and it is not a toast". Stale on both counts as
       of the third pass: four of the 122 carry aria-live, and TWO of those four
       are toasts that got it right — guild.html:68 and guilds.html:74 are both
       `<div id="toast" role="status" aria-live="polite"></div>`. Neither page
       mounts .sanctuary, so neither collides with the class below; copy the two
       attributes from them rather than reasoning it out again.)
     • .dialog needs `role="dialog" aria-modal="true"`, a focus trap and an Esc
       handler. Nothing in this repo has one; <dialog> is used nowhere.
     • .skel should carry `aria-hidden="true"` with the live region announcing
       "loading" separately, or a reader hears the placeholder bars.
     • .skip-link needs an actual <a class="skip-link" href="#main"> as body's
       first child plus an id="main" target. Both are edits to files this slice
       does not own — AND, as of the third pass, BOTH ARE NOW DONE on one page:
       watch.html:46 is exactly that anchor and watch.html:48 is
       `<main class="wrap" id="main">`. watch.html mounts and writes no
       skip-link CSS of its own, so the rule in section B is the only thing
       parking that link at top:-100px. It stopped being a dormant primitive
       and became load-bearing markup on a live page; delete it and watch.html
       grows a stray visible link above its header. NOTE: topbar.js has
       since grown its own `.tb-skip` inside the injected bar, which covers the
       universal case — different class name, so the two do not collide. This
       .skip-link is for a page that wants its own, not a competing mount.
   ============================================================================ */

/* ── A · FOCUS ───────────────────────────────────────────────────────────────
   The one real defect in the palette was that focus was decorative. Line 53
   trades the UA outline for a 3px rgba(243,217,164,.12) glow which, composited
   over --bg, measures 1.24:1 — below perception, not just below the 3:1 that
   WCAG 2.2 SC 1.4.11 asks of a UI indicator. Nothing else — not .btn, not
   .pill, not a link — had any focus rule at all.
   --gold-2 (#caa96a) measures 9.01:1 on --bg, so a 2px ring is unambiguous. It
   is NOT, as the first cut of this comment claimed, the only token that clears
   3:1 — every SOLID token does (--ink 17.39, --gold 14.65, --champagne 13.30,
   --teal 13.26, --rose 12.63, --ink-dim 11.64, --violet-2 11.55, --violet 9.14,
   --ink-soft 7.82, --faint 5.27). What is true, and what the choice actually
   rests on, is that the tokens you would reach for FIRST — the hairlines — all
   fail: composited over --bg, --edge is 1.29:1, --hair 1.35:1 and --edge-2
   2.34:1. So "draw the ring in the border colour" is the one option WCAG
   refuses. --gold-2 is also already .btn's border, so the ring reads as part of
   the component rather than as a second accent.
   This ADDS the outline and changes nothing else: line 53's border swap and glow
   both stay. The ring is stated twice — once as a catch-all for anything
   focusable, once as an explicit element list — because the catch-all is (0,2,0)
   and would LOSE to line 53's (0,2,1) `outline:none`. The list ties that weight
   and wins on source order; it does not out-weigh it.
   No border-radius is set here on purpose: outlines follow the element's own
   corner, so .pill keeps --radius-pill and .btn keeps its capsule. */
.sanctuary :focus-visible{ outline:2px solid var(--gold-2); outline-offset:2px; }
.sanctuary a:focus-visible, .sanctuary .btn:focus-visible, .sanctuary .pill:focus-visible,
.sanctuary button:focus-visible, .sanctuary summary:focus-visible, .sanctuary [tabindex]:focus-visible,
.sanctuary input:focus-visible, .sanctuary textarea:focus-visible, .sanctuary select:focus-visible{
  outline:2px solid var(--gold-2); outline-offset:2px; }

/* ── B · THE UNNAMEABLE ──────────────────────────────────────────────────────
   These two rules are the ONLY declaration of either name in the repo, and both
   already have consumers. Re-grepped on the third pass:
     • `.sr-only` — used at news.html:45, :105 and :111 (a
       `<span class="sr-only"> (opens in a new tab)</span>` on every link that
       opens a new tab), and news.html mounts. No other file declares the rule.
     • `.skip-link` — seated at watch.html:46 against `id="main"` at :48, and
       watch.html mounts. No other file declares the rule.
   So this section is NOT the speculative primitive the earlier text ("there is
   no .sr-only anywhere in 120 pages") described. Deleting either rule does not
   remove an unused affordance, it exposes hidden text on a shipped page. What
   the earlier sentence was reaching for, and what is still true, is the reason
   the rules exist: before them every icon-only control in the storefront was
   either titled or nameless.
   .skip-link uses :focus (not :focus-visible) — a bypass control must appear
   for ANY focus, including programmatic. */
.sanctuary .sr-only{ position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; }
.sanctuary .skip-link{ position:absolute; left:var(--s2); top:-100px; z-index:100;
  font-family:var(--serif); font-size:var(--t-body); text-decoration:none;
  background:var(--bg2); color:var(--gold); border:1px solid var(--gold-2);
  border-radius:var(--radius); padding:10px 18px; }
.sanctuary .skip-link:focus{ top:var(--s1); }

/* ── C · LAYOUT ──────────────────────────────────────────────────────────────
   Four primitives, no more. The floor is a real 1366x600 Chromebook window, so
   every one of these is either intrinsic (auto-fit) or collapses at a stated
   width; nothing here assumes 1920.
   .stack is the owl, not flex+gap, deliberately: adjacent block margins still
   collapse, so a .stack around an h2 (which carries margin --s3/--s2 of its
   own) gets max(its margin, --s2) instead of both stacked. It also composes
   inside .card, inside a grid cell, inside anything.
   .cluster is NOT a duplicate of .row: .row is the tight (--s1) inline group
   and has no justification. .cluster is the --s2 group that can push its tail
   to the right — the title-left / actions-right shape .row cannot express.
   All three weigh (0,0,0): the whole ancestor chain is inside the :where() and
   the target is `*`. `.tight`/`.loose` beat the base purely on source order, so
   ANY rule a page writes for its own child element wins. (The first cut wrote
   `:where(.sanctuary) .stack.tight > * + *` at (0,2,0), which would have beaten
   a page's plain `.thing{margin-top:…}` inside a tight stack.) */
:where(.sanctuary .stack) > * + *{ margin-top:var(--s2); }
:where(.sanctuary .stack.tight) > * + *{ margin-top:var(--s1); }
:where(.sanctuary .stack.loose) > * + *{ margin-top:var(--s3); }

:where(.sanctuary) .cluster{ display:flex; flex-wrap:wrap; gap:var(--s2); align-items:center; }
:where(.sanctuary) .cluster.between{ justify-content:space-between; }
:where(.sanctuary) .cluster.end{ justify-content:flex-end; }
:where(.sanctuary) .cluster.baseline{ align-items:baseline; }

/* The workhorse. 240px matches the value already most common in the wild
   (call.html:16, arcade.html:13). min(240px,100%) rather than a bare 240px so a
   narrow phone can never blow the track out past the viewport. */
:where(.sanctuary) .grid{ display:grid; gap:var(--s2);
  grid-template-columns:repeat(auto-fit,minmax(min(240px,100%),1fr)); }
:where(.sanctuary) .grid.narrow{ grid-template-columns:repeat(auto-fit,minmax(min(160px,100%),1fr)); }
:where(.sanctuary) .grid.wide{ grid-template-columns:repeat(auto-fit,minmax(min(320px,100%),1fr)); }

/* Countable columns, for when auto-fit's guess is wrong. minmax(0,1fr) instead
   of 1fr so long words and <pre> cannot force the track wider than its share.
   720px is not a new breakpoint — it is one of the EIGHT the 122 pages already
   use (480/620/640/680/720/760/800/820; the first cut listed seven and dropped
   760). Re-derived on the third pass by scanning @media PRELUDES only, which is
   the only scan that answers this — a bare grep for `min-width:` also catches
   grid track sizing and is off by a mile: as media queries the storefront uses
   min-width at 760 and 820 and max-width at 480/620/640/680/720/800/820, which
   is those eight. 920px IS new — it appears as a media query nowhere in the 122
   (the two `920` hits, achievements.html:9 and arcade.html:9, are `.wrap{
   max-width:920px}` declarations, not queries) — and it is spelled twice below,
   not once: once for
   .grid-3 and once for .sidebar, deliberately, because they turn at the same
   width. (topbar.js carries two more of its own: max-width:460px and
   max-height:640px. Neither interacts with anything here.) */
:where(.sanctuary) .grid-2, :where(.sanctuary) .grid-3{ display:grid; gap:var(--s2);
  grid-template-columns:minmax(0,1fr); }
@media (min-width:720px){
  :where(.sanctuary) .grid-2, :where(.sanctuary) .grid-3{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (min-width:920px){
  :where(.sanctuary) .grid-3{ grid-template-columns:repeat(3,minmax(0,1fr)); }
}

/* main + rail. One column until there is honestly room for two — at the 1366
   floor that is 1fr + 300px inside the 820px .wrap, which still leaves the main
   column ~490px. The rail is `.aside` and not `.rail`, because .rail is taken
   (write.html:18, continuity.html:14). */
:where(.sanctuary) .sidebar{ display:grid; gap:var(--s3); align-items:start;
  grid-template-columns:minmax(0,1fr); }
:where(.sanctuary) .sidebar > *{ min-width:0; }
@media (min-width:920px){
  :where(.sanctuary) .sidebar{ grid-template-columns:minmax(0,1fr) 300px; }
  :where(.sanctuary) .sidebar.rail-left{ grid-template-columns:300px minmax(0,1fr); }
  /* both (0,0,0) — the entire chain lives inside the :where(), and nothing else
     in this file sets `order` or `position` on a rail. They were (0,3,0), which
     would have out-weighed a page's own `.aside{order:…}`. */
  :where(.sanctuary .sidebar.rail-left > .aside){ order:-1; }
  :where(.sanctuary .sidebar > .aside.sticky){ position:sticky; top:56px; }
}

/* ── D · SURFACE ─────────────────────────────────────────────────────────────
   .sheet promoted from gtable.html:14 / onepagers.html:14 — the same five
   declarations (border, border-radius, padding, margin, background, reordered
   here; the values are identical) that 11 of the 12 mounted .sheet pages already
   ship byte-for-byte as each other. clockwork.html:25
   is the twelfth and DIFFERS (padding:14px 16px; margin:12px 0;
   background:rgba(0,0,0,.2)); it keeps every one of those, because this rule is
   `:where()`-scoped at (0,1,0) and clockwork's own rule is later. It is the
   flatter sibling of .card: a page SECTION (no gradient, no shadow, own vertical
   rhythm), which is why it prints as a page-break-avoiding block up in
   @media print. Adopting it changes nothing on any of the twelve. */
:where(.sanctuary) .sheet{ background:var(--panel); border:1px solid var(--edge);
  border-radius:var(--radius); padding:var(--s3); margin:var(--s3) 0; }
/* The leading-HEADING reset, and only a heading. (0,1,1): it ties `.sanctuary h2`
   (line 34, which sets margin-top:var(--s3)) and wins on source order — that is
   the entire job — and it ties a page's own `.sheet h2{margin-top:0}` and LOSES
   on source order, which is the entire policy. Nine of the twelve .sheet pages
   hand-wrote exactly that rule (almanac:15, gtable:15, medic:20, memory:15,
   onepagers:15, school:15, spark:15, well:15, workshop:15) with the same value,
   so they see no change. The other three (clockwork, identify, lookup) never
   wrote it — and never need it: their .sheet blocks are injected at runtime and
   not one of them leads with a heading. Cited by the line that OPENS each block,
   since the first cut mixed opening lines and child lines: clockwork emits six
   (:182, :189, :190, :199, :217, :258) leading with <p>, <p>, <p>, <p>, <b> and
   a chip <span> in that order; identify:133 and lookup:152 lead with
   `<div class="cat">` and put the h2 second; identify:123/:124 and
   lookup:139/:163 lead with <p>. Re-checked the other nine too: every static
   .sheet on all nine leads with an <h2> and nothing else — no h1/h3/h4/h5/h6
   leads a .sheet anywhere, so the `:is(h1…h6)` arm can only ever land on an h2
   that a page has already zeroed itself. So this rule's effect on all twelve
   live .sheet pages is ZERO. It exists for the thirteenth page — the next one,
   which should not have to rediscover it.

   DELIBERATELY NOT `> :first-child`. The first cut wrote
   `:where(.sanctuary) .sheet > :first-child{margin-top:0}` and it was wrong twice,
   both measured in a browser rather than argued:
     (a) SPECIFICITY. At (0,2,0) it out-weighed the page. onepagers.html:16
         declares `.sheet table{ …; margin:8px 0 }` at (0,1,1); a <table> leading
         a .sheet there computed margin-bottom:8px (page wins) and margin-top:0px
         (shell wins). A shell primitive must never split a page's shorthand.
     (b) BLAST RADIUS. It reached live content nobody asked it to. THREE mounted
         pages inject .sheet blocks that lead with a <p> and never wrote a reset
         of their own — clockwork.html:182/:189/:190/:199, identify.html:123/:124
         and lookup.html:139/:163 — and every one of those paragraphs computed
         margin-top:0px instead of the UA's 1em. The first cut disclosed only two
         of the three; clockwork was missed entirely. Restyling shipped pages is
         a taste ruling, which is exactly why the matching `.card > :first-child`
         was refused below — refusing it for .card while shipping it for .sheet
         was incoherent. Both are refused now. A page that wants the general
         reset writes its own `.sheet > :first-child`, and wins.
   Net effect of section D on the twelve live .sheet pages: zero, re-measured. */
:where(.sanctuary .sheet) > :is(h1,h2,h3,h4,h5,h6):first-child{ margin-top:0; }
/* NOT DONE ON PURPOSE: the matching `.card > :first-child{margin-top:0}`. .card is
   an EXISTING component (line 40). Re-measured blast radius in the static markup
   of the 10 mounted pages that use .card: it would zero FIVE elements —
   p.lead on bug-court:39, outflow-admin:30 and redteam-guild:37 (14px each,
   their font-size is overridden inline), p.lead on wheel:28 (16px), and h2 on
   watch:63, which would drop from `.sanctuary h2`'s var(--s3)=24px to 0.
   The first cut said "5 live elements … two h3 on pigeonpost": those two h3
   already carry inline style="margin-top:0" (pigeonpost.html:62,:67) so they
   cannot move, and wheel.html was missed. The second pass then said FOUR — and
   was overtaken within the hour: watch.html adopted `class="sanctuary"` mid-
   audit and brought a fifth, while news.html left the static set by moving its
   cards behind a runtime `say(msg,'card')` helper. Note also that index.html
   already declares `.card h2:first-child{margin-top:0}` (index.html:89, at
   (0,2,1)), so its two section headings are NOT in this count — a page that
   wants the reset writes it, which is the argument for leaving it to pages.
   Runtime-injected cards (outflow builds 16 in JS) are NOT in the count —
   scripts were disabled for the measurement. Either way it is a taste ruling
   on shipped pages: Nick's call, not an additive shell change. */

/* ── E · THE OVERLAY LAYER ───────────────────────────────────────────────────
   For the command palette and the wheel. Re-counted z-index budget in this repo:
   page rails 30 (write.html:18), the topbar itself (.tbar / .tbar-u) 40
   (topbar.js:125), outflow's #toast 50 (outflow.html:66), the topbar's own
   dropdown (.tb-menu) 60 (topbar.js:181) and — missed by the first cut — the
   topbar's skip link at 61 (topbar.js:172). The scrim therefore sits at 80,
   clear of all five and not tied with the dropdown, and .toast at 90 so a
   confirmation still reads over an open dialog. Section B's own .skip-link is
   deliberately above BOTH the scrim and the toast, at 100: a bypass control a
   backdrop can bury is not a bypass control.
   THE CEILING THIS LEDGER DID NOT NAME, and the third pass caught it: outflow
   is a MOUNTED page and it already runs its own modal ABOVE everything here —
   `.gmodal{position:fixed;inset:0;z-index:130}` (outflow.html:157) plus a
   runtime backdrop built with the same 130 (outflow.html:1084). So "clear of
   all five" describes the CHROME, not the whole repo: on outflow, an open
   .gmodal covers a .scrim (80), a .dialog inside it, a .toast (90) AND section
   B's .skip-link (100). That is arguably correct — a page's own modal should
   sit over the shell's furniture — but it is a real ordering nobody chose
   deliberately. If the shell layer is ever seated on outflow, pick the winner
   there on purpose rather than discovering it. Nothing in this file changes to
   accommodate it; this is a disclosure, not a fix.
   Hidden by default and shown with `.on`, matching this file's own existing
   `.pill.on` convention. `[hidden]` is listed after `.on` so the attribute wins
   the tie and always beats the class — a hidden dialog must stay hidden.
   The scrim colour is a literal: it is --bg (#08060e) at 78%. The palette has
   no alpha-ground token and the token contract is frozen, so the value is
   spelled out here rather than invented as a new --scrim. */
:where(.sanctuary) .scrim{ display:none; position:fixed; inset:0; z-index:80;
  background:rgba(8,6,14,.78); padding:var(--s3); place-items:center;
  overflow:auto; overscroll-behavior:contain; }
:where(.sanctuary) .scrim.on{ display:grid; }
:where(.sanctuary) .scrim.top{ place-items:start center; padding-top:10vh; }
:where(.sanctuary) .scrim[hidden]{ display:none; }

/* --panel/--panel2 are both translucent, so the gradient is layered OVER solid
   --bg2 — otherwise the dialog would show the page through itself.
   max-height is stated twice on purpose: dvh where supported, vh as the
   fallback for anything that drops the line. At the 600px floor that is a
   536px box with its own scroll, not a dialog running off the screen.
   box-sizing is explicit because this file has never set it globally and NO
   mounted page does either — re-counted on the third pass, `box-sizing` appears
   in only 2 of the 33 (chess.html:39, clockwork.html:35) and both are
   single-element rules, not a reset. The first cut implied those 2 set it
   globally; they do not. (Said "2 of the 31" until the third pass — the 2 held,
   the denominator did not.) */
:where(.sanctuary) .dialog{ box-sizing:border-box; width:min(560px,100%);
  max-height:calc(100vh - var(--s5)); max-height:calc(100dvh - var(--s5)); overflow:auto;
  background:linear-gradient(180deg,var(--panel),var(--panel2)), var(--bg2);
  border:1px solid var(--edge-2); border-radius:var(--radius); padding:var(--s3);
  box-shadow:0 1px 0 rgba(243,217,164,.05) inset, 0 40px 90px -40px rgba(0,0,0,.9); }
:where(.sanctuary) .dialog.wide{ width:min(760px,100%); }
/* Unlike .sheet, .dialog is a name no mounted page owns, so the shell may keep
   the general first-child reset here — but not at the weight it had. (0,1,0)
   for the general case, (0,1,1) for the heading case, which is the only one
   that has to out-rank `.sanctuary h2` on line 34. Both were (0,2,0). */
:where(.sanctuary .dialog) > :first-child{ margin-top:0; }
:where(.sanctuary .dialog) > :is(h1,h2,h3,h4,h5,h6):first-child{ margin-top:0; }
/* (0,1,0), was (0,2,0). index.html:92 — a mounted page — declares a plain
   `.foot{max-width;margin;padding}` at (0,1,0); at the old weight a .foot nested
   in a dialog would have lost its own page's rule to this one. (The first cut
   cited index.html:71, which is a `#seekResults` rule with no `.foot` in it at
   all. index.html has two other `.foot` rules and neither is the one that
   matters: :43's `#page, .foot` arm is also (0,1,0) but only sets position and
   z-index, which this rule never touches, and :79 targets `.foot .violet`, a
   different element. Note also that :92's `margin:0 auto` shorthand and this
   rule's `margin-top` longhand tie at (0,1,0), so the page's later `margin-top:0`
   wins outright — no repeat of the split-shorthand defect from section D.) */
:where(.sanctuary .dialog) .foot{ display:flex; flex-wrap:wrap; gap:var(--s1);
  justify-content:flex-end; margin-top:var(--s3); }

/* The palette's result list. Works as <ul>/<ol> or a bare div of <button>s;
   the active row is either `.on` or aria-selected, so keyboard state and visual
   state can be the same attribute instead of two things drifting apart.
   Weights: the list itself (0,1,0); rows (0,0,0); :hover and .on / aria-selected
   (0,1,0), so the active state still beats hover on source order exactly as it
   did when both were (0,2,0) — but a page's own row rule now beats all of them.
   KNOWN LIMIT: an <a> row keeps `.sanctuary a`'s gold, because that rule is
   (0,1,1) and this block is capped at (0,1,0) on purpose. Use <button> rows, or
   colour the links from the page. */
:where(.sanctuary) .menu{ list-style:none; margin:var(--s2) 0 0; padding:0;
  max-height:46vh; overflow:auto; }
:where(.sanctuary .menu) > *{ box-sizing:border-box; display:block; width:100%; text-align:left;
  font-family:var(--serif); font-size:var(--t-body); color:var(--ink-dim); text-decoration:none;
  background:none; border:0; border-radius:10px; padding:9px 11px; cursor:pointer; }
:where(.sanctuary .menu) > :hover{ background:var(--panel2); color:var(--ink); }
:where(.sanctuary .menu) > .on,
:where(.sanctuary .menu) > [aria-selected="true"]{ background:var(--panel); color:var(--gold);
  box-shadow:inset 0 0 0 1px var(--edge-2); }

/* ── F · STATUS ──────────────────────────────────────────────────────────────
   .toast is outflow.html:65's #toast lifted to a class, same geometry and same
   tokens, so the TEN separate toast implementations have somewhere to converge
   (course:21, credit:18, guild:21, guilds:18, highstreet:26, lightforge:93,
   market:23, outflow:65, pay-crypto:34, site:22 — the first cut said six). That
   convergence is worth having: EIGHT of the ten are one byte-identical
   hand-copied `#toast` block, and only outflow:65 and lightforge:93 differ from
   it. Just one of the ten mounts .sanctuary — outflow — and it spells its own as
   `#toast`, an id, so this class cannot reach it and an id out-weighs it anyway
   if a page ever carries both.
   pointer-events:none because a status message must never eat a click. It fades
   with opacity+visibility rather than display, so the reduced-motion rule on
   line 56 kills the transition and it simply appears — it never disappears. */
:where(.sanctuary) .toast{ box-sizing:border-box; position:fixed; left:50%; bottom:22px;
  transform:translateX(-50%); z-index:90; max-width:min(92vw,var(--maxw)); pointer-events:none;
  background:var(--bg2); border:1px solid var(--edge-2); color:var(--gold);
  font-size:var(--t-body); padding:10px 18px; border-radius:var(--radius-pill);
  box-shadow:0 20px 50px -30px rgba(0,0,0,.9);
  opacity:0; visibility:hidden; transition:opacity .18s ease, visibility .18s; }
:where(.sanctuary) .toast.on{ opacity:1; visibility:visible; }

/* Promoted from clockwork.html:43. NOT "the only empty state in the platform" —
   the first cut said that and it was wrong. Five files carry a RULE matching
   `.empty` (clockwork:43, grimoire:23, impact-panel:22 declare a bare one,
   arcana:53 scopes `.book .empty`, ninebells:28 writes `#play.empty`), and
   clockwork is the only one of the five that mounts .sanctuary.
   --ink-dim (11.64:1) not --faint, because an empty state is the only thing on
   the screen when it shows; it is the content, not a footnote.

   BLAST RADIUS — TWO PAGES, NOT ONE. The first two passes stopped at "clockwork
   is the only page this rule can reach", which is the wrong question: a page
   does not need a `.empty` RULE to be reached, it needs `class="empty"` MARKUP.
   Grepping the markup of the 33 mounted pages instead of their stylesheets
   (`grep -n 'class="[^"]*\bempty\b"' $(grep -l 'class="sanctuary' *.html)`)
   finds two:
     • clockwork.html — eight runtime injections (:176, :193, :211, :212, :215,
       :252, :253, :256), governed by its own :43. MEASURED, and unchanged from
       the earlier disclosure: `:where()` lets a page win every property it
       DECLARES, but cannot stop this rule contributing one the page never
       declared. :43 sets only colour, font-size and padding — all three survive
       (padding stays 20px 6px) and the element newly picks up text-align:center
       and line-height:1.6.
     • events.html:135 — STATIC markup, `<div class="empty"><span class="kick">
       scripting is off</span>…` inside a <noscript>. events.html mounts and
       declares NO `.empty` rule of its own, so unlike clockwork it takes BOTH
       rules below WHOLE: padding var(--s4) var(--s2), centring, --ink-dim,
       --t-body, line-height 1.6, and `.kick` promoted to a block with an --s1
       gap. Read the markup and that is plainly what it was written against —
       it uses .kick nested in .empty exactly as this pair defines them — so
       this is adoption, not collateral. It is still a live page changing
       appearance, so it is named here rather than left to be discovered.
   That is the whole blast radius across the 33 mounted pages, re-measured on
   the third pass. If centring it is unwanted, delete the two lines below —
   but note that events.html:135 then loses its layout entirely, which the
   earlier "nothing else moves" would have let you believe was free.
   `.empty .kick` is (0,1,0), was (0,2,0). */
:where(.sanctuary) .empty{ padding:var(--s4) var(--s2); text-align:center;
  color:var(--ink-dim); font-size:var(--t-body); line-height:1.6; }
:where(.sanctuary .empty) .kick{ display:block; margin-bottom:var(--s1); }

/* Skeletons. Under prefers-reduced-motion line 56 stops the sweep and the bar
   holds as a static panel tint — it degrades to a placeholder, not to nothing,
   which is the whole reason the shimmer lives in background-position and not in
   opacity. Give .skel an explicit height or use .line/.block/.circle. */
:where(.sanctuary) .skel{ display:block; min-height:1em; border-radius:8px;
  background:linear-gradient(90deg,var(--panel2) 0%,var(--panel) 50%,var(--panel2) 100%);
  background-size:200% 100%; animation:sanct-skel 1.4s linear infinite; }
:where(.sanctuary) .skel.line{ height:.85em; margin:9px 0; }
:where(.sanctuary) .skel.line.short{ width:45%; }
:where(.sanctuary) .skel.block{ height:120px; }
:where(.sanctuary) .skel.circle{ width:44px; height:44px; border-radius:50%; }
@keyframes sanct-skel{ from{ background-position:200% 0; } to{ background-position:-200% 0; } }

/* ── G · KEYCAPS ─────────────────────────────────────────────────────────────
   For the command palette's hints. The font stack is the one literal in this
   block that is not a token: a keycap has to be monospace and the contract has
   no mono token, so it copies the stack wavestone.html:50 already uses rather
   than inventing a second one — and as of this pass it genuinely is that stack.
   THE ONE CODE CHANGE IN THE SECOND AUDIT: this rule shipped
   `ui-monospace,SFMono-Regular,Menlo,Consolas,monospace` while the comment
   claimed to match wavestone, which ships the shorter `ui-monospace,monospace`.
   The comment described a stack the file did not contain, and the policy it was
   citing ("do not invent a second one") was the thing being broken — so the
   declaration was corrected to wavestone's, verbatim, rather than the claim
   being softened. Zero live effect either way: <kbd> appears on none of the 33
   mounted pages (re-grepped on the third pass; the count was "31"), and
   wavestone is not one of them. Size, colour, border: tokens.
   CN6 UPDATE: the contract now HAS a mono token, so the copied literal above is
   retired — this is the token, and `ui-monospace,monospace` survives inside
   --mono's own fallback tail. Everything in this rule is a token again. */
:where(.sanctuary) kbd{ display:inline-block; white-space:nowrap;
  font-family:var(--mono);
  font-size:var(--t-cap); line-height:1.5; padding:2px 7px; border-radius:6px;
  background:var(--panel2); border:1px solid var(--edge-2); border-bottom-width:2px;
  color:var(--ink-dim); }

/* ── H · ANCHORS UNDER THE STICKY BAR ────────────────────────────────────────
   topbar.js injects an in-flow sticky bar at z-index 40 whose box computes to
   ~44px (8px padding top and bottom, a 13px/1.2 link with 6px of its own, plus
   a 1px border — topbar.js:125-139), and BEFORE this file there was no
   scroll-margin-top or scroll-padding-top anywhere in the storefront, so every
   in-page anchor on every page landed underneath it. 56px is the bar plus air. It also fixes scrollIntoView(), which :target alone would not.
   Over-clearing is harmless here; under-clearing hides the heading.
   The heading arm was three separate `h1[id]`/`h2[id]`/`h3[id]` selectors at
   (0,1,1) each — type plus attribute — which would have out-weighed a page's own
   `.thing{scroll-margin-top:…}`. Folded into a `:where()`-ed type list so only
   the `[id]` counts: (0,1,0), same as `:target` beside it. */
:where(.sanctuary) :target,
:where(.sanctuary) :where(h1,h2,h3)[id]{ scroll-margin-top:56px; }

/* ── I · FRONT-SIMPLE1 — the calmer door ─────────────────────────────────────
   front-simple.js marks <body data-front-simple> for a first-timer guest, so
   the rich homepage widgets (presence, journey strip, ornament) sit quieter
   behind the main board + the pillar-card hand. Honest staging: nothing is
   removed, only dimmed to the faint edge tone — a stranger still sees the
   whole surface, just without the noise competing with the seven doors. */
body[data-front-simple] .front-simple-rail{ margin-top:var(--s2); }
body[data-front-simple] .front-simple-rail .pillar-card{
  border-color:var(--edge-2);
}
@media (prefers-reduced-motion: no-preference){
  body[data-front-simple] .front-simple-rail .pillar-card{
    transition:border-color .2s ease, background .2s ease, transform .2s ease;
  }
}
