﻿/* ── Ginger Ninjalytics design tokens (ADR-001 / DRIP ADR-0007) ──────────────────
   Tokens are defined only here, in the shell. Module RCLs consume these
   variables and must never define their own.

   Load order is load-bearing: this file must load AFTER bootstrap.min.css
   (see App.razor) — the Bootstrap remaps in section 2 beat Bootstrap's own
   :root/[data-bs-theme] blocks purely by source order at equal specificity.

   THREE ATTRIBUTES ON <html>, AND THEY ARE NOT THE SAME THING (ADR-044 §2):
     data-mode    the user's PREFERENCE — auto | light | dark. Server-rendered,
                  never written by script, and nothing in CSS selects on it.
     data-scheme  the RESOLVED ground — light | dark. This is what CSS keys on.
                  Server-rendered when the preference is not `auto`; the head
                  script in App.razor resolves `auto` against the OS before the
                  first paint, which is the whole reason that script is inline.
     data-theme   the palette key (ADR-003 claim). A palette block outranks the
                  light ground by source order, so §1b must stay BELOW §1a —
                  both are (0,2,0) and only order separates them.

   Token NAMES are a runtime contract, not just CSS: game.js:749, game.words.js:70
   and contact-gauntlet.js:186 read --gn-accent / --gn-text / --gn-text-muted /
   --gn-danger / --gn-bg / --gn-font / --gn-word-* off the computed style. Rewriting
   a value is free; renaming one silently repaints or blanks a game. */

/* ── 1. Token values — the dark default ground (ADR-044 §2: charcoal + verdigris)
   Every colour below is checked against the ground it actually sits on: body text
   and semantic hues ≥4.5:1 on --gn-bg / --gn-surface / --gn-surface-2, control
   boundaries ≥3:1, and --gn-on-accent ≥4.5:1 on every fill including its hover and
   active mixes. S2.2 authors the other ten palettes and re-checks these two. */
:root {
    color-scheme: dark;
    /* The type scale's root. A percentage (not rem) so it resolves against the
       reader's own browser default rather than against itself — everything below
       is in rem, so this single value is the user's type-scale step (ADR-044 §3). */
    --gn-fs-root: 100%;
    font-size: var(--gn-fs-root);
    --gn-bg: #15181b;
    --gn-surface: #1d2125;
    --gn-surface-2: #262c31;
    --gn-border: #333b41;
    /* Separators may be quiet; a CONTROL boundary may not (WCAG 1.4.11 wants 3:1).
       Two tokens rather than one loud border on every card and comment rule. */
    --gn-border-strong: #6c7883;
    --gn-text: #e2e7ea;
    --gn-text-muted: #98a3ab;
    --gn-accent: #2fa89f;
    --gn-accent-hover: #3ec4ba;
    /* THE SECOND HUE (owner, 2026-08-10). Derived, not picked — see the recipe below §1a.
       Used sparingly and only to draw an eye: selection controls, the DAFUQ glow, the
       selected state of a widget. Never a second brand colour, never a button fill. */
    --gn-accent-2: #a784c7;
    --gn-on-accent-2: #04120f;
    /* The ink for ANY filled control — accent or semantic. Dark here because this
       theme's fills are bright; the light ground flips it to white. */
    --gn-on-accent: #04120f;
    /* Which way a fill moves on hover/active. Darkening a bright fill drags it
       toward the dark ink and drops btn-danger's active state to 4.48:1 — so a
       dark ground lightens instead. One token, both directions correct. */
    --gn-fill-shade: white;
    --gn-link: #5accc2;
    --gn-link-hover: #84dcd4;
    --gn-success: #54b988;
    --gn-danger: #f0736f;
    --gn-warning: #e2b04a;
    --gn-info: #5cb8dc;
    /* Inline <code>. Bootstrap ships a fixed pink here and nothing routed it
       through the tokens, so it survived every persona theme unchanged. */
    --gn-code: #e9a06d;
    --gn-focus-ring: rgba(47, 168, 160, .32);
    /* ADR-044 §4's default, and it is only honest now that §1c self-hosts the face: naming
       Cascadia Mono in a stack used to mean Windows saw it, macOS fell through to SF Mono
       and Linux to whatever `monospace` resolves to. The fallbacks stay for the moment
       between first paint and the file arriving. Readers override this per §1c. */
    --gn-font: "Cascadia Mono", ui-monospace, Consolas, monospace;
    --gn-font-heading: var(--gn-font);
    /* L3's geometry, not a preference: the console look IS hard edges. Kept as three
       tokens rather than deleted, because Bootstrap's --bs-border-radius-* remap in §2
       needs something to point at and a later palette could soften them. */
    --gn-radius: 0;
    --gn-radius-sm: 0;
    --gn-radius-lg: 0;
    --gn-shadow: none;
    --gn-sidebar-bg: #1d2125;
    --gn-sidebar-text: #c8d0d5;
    /* Literal RGB triplets Bootstrap consumes via rgba(var(...)) — cannot be
       derived from the hex tokens; keep in sync with --gn-bg/--gn-text/--gn-danger. */
    --bs-body-bg-rgb: 21, 24, 27;
    --bs-body-color-rgb: 226, 231, 234;
    --bs-danger-rgb: 240, 115, 111;
    /* Spacing scale */
    --gn-space-1: .25rem;
    --gn-space-2: .5rem;
    --gn-space-3: 1rem;
    --gn-space-4: 2rem;
    --gn-space-5: 4rem;
    /* ── Type scale (ADR-044 §4) ───────────────────────────────────────────────
       There was none: sizes were literal rem values scattered through this file,
       so "make the text bigger" had nothing to turn. Every step is a rem, so
       --gn-fs-root above moves the whole scale at once. Ratio ≈1.2. */
    /* The step below xs, and it is ON the ratio (.75 / 1.2), not a special case: L3's console
       type leans on it for every micro-label — column heads, kickers, tag chips, status pills —
       and the alternative was a literal, which would sit outside --gn-fs-root and stop moving
       when the reader changes their type size (ADR-044 §4). Added by row S4.1. */
    --gn-fs-2xs: .625rem;
    --gn-fs-xs: .75rem;
    --gn-fs-sm: .875rem;
    --gn-fs-md: 1rem;
    --gn-fs-lg: 1.125rem;
    --gn-fs-xl: 1.35rem;
    --gn-fs-2xl: 1.6rem;
    --gn-fs-3xl: 2rem;
    --gn-fs-4xl: 2.5rem;
    --gn-lh-tight: 1.25;
    --gn-lh: 1.6;
    --gn-measure: 68ch;   /* ADR-042 §7 — ch, never px: every font has its own character width */
    /* ── Responsive floors (ADR-042 §8) ────────────────────────────────────────
       The numbers, in one place, so no later row re-derives them. Breakpoints are
       deliberately NOT here: custom properties cannot be used in @media. */
    --gn-touch: 44px;       /* minimum touch target on a coarse pointer */
    --gn-touch-gap: 8px;    /* minimum separation between two of them */
    --gn-bar-h: 56px;       /* fixed top bar — S1.2 consumes this */
    --gn-strip-h: 56px;     /* low strip, IDLE state; §3a raises it under body.gn-playing */
    /* The shell's horizontal gutter — bar, main and strip share it, so their
       contents line up vertically at every width. */
    --gn-gut: clamp(.75rem, 3vw, 2rem);
    /* Word-role highlight colours (ADR-008): the game glows categorized blog words in these.
       Intentionally constant across persona themes — a colour means one thing everywhere.
       In Paragraph Parkour the colour IS the material, so each one now looks like what it does:
       adjective is frost, verb is fire, and both of those spread to the words around them. */
    --gn-word-adjective: #dceeff;   /* frost — near-white blue, and it creeps */
    --gn-word-contraction: #35c8d6; /* sticky — the cling surface */
    --gn-word-jargon: #ff5c7a;      /* jargon — the towers you have to climb */
    --gn-word-noun: #5aa0ff;        /* springboard */
    --gn-word-verb: #ff6a1a;        /* fire — hotter than the old amber, and it spreads */
    --gn-word-reserved: #b57bff;    /* profanity — turrets */
    --gn-word-hedge: #9d94c4;       /* hedging — platforms that will not commit to existing */
    /* AI usage label (ADR-040). Theme-invariant for the same reason as --gn-word-* above: a
       disclosure about how something was made has to mean one thing under every persona, or it
       is not a disclosure. Absent from every :root[data-theme] block on purpose — that omission
       IS the invariance.
       The plate is doing real work, not decoration. It used to be justified against
       hot-dog-stand's #b71c1c body; that theme is retired, and ADR-044 §3 puts the lock on
       better grounds — twelve user-selectable palettes are twelve different grounds, and a
       token that must mean one thing in all of them needs its own plate more than ever, not
       less. The tuner overrides the two hues per-instance. */
    /* DAFUQ's glow (ADR-045 §10, row S3.8). NOT theme-invariant like the two families above,
       and derived rather than authored twelve times: --gn-link is each palette's secondary
       interactive colour and --gn-link-hover is its BRIGHT form — which is what a glow wants
       (owner, 2026-08-10). Already checked against that palette's ground, so this follows the
       theme for free and a palette that wants its own glow overrides this one line. The glow is
       a halo AROUND text that stays --gn-text, so it carries no text-contrast obligation of its
       own — the magnifier glyph, not the colour, is what satisfies §2 rule 11. */
    /* Now the second hue rather than a brighter primary — the glow's whole job is drawing an
       eye, and --gn-link-hover could only ever be the accent turned up (owner, 2026-08-10). */
    --gn-dafuq-glow: var(--gn-accent-2);
    --gn-c3-plate: #1b2029;         /* only the breakdown's rings still sit on this */
    /* THE TWO HUES NOW FOLLOW THE PALETTE (owner, 2026-08-11). This reverses ADR-040's
       theme-invariance for --gn-c3-human/--gn-c3-ai; the amendment on ADR-040 carries the
       reasoning and the cost. Two things made it defensible rather than merely asked for:
       the plate came off these rings in 2026-08-07, so they already sit on the page ground
       and the fixed cyan/pink were already under 3:1 on grounds near their own hue — the
       accents are the one pair that HAS been measured against every ground (ADR-044,
       accent2.py). And #14b0d9 sat 17deg off the default accent, which read as a failed
       match rather than as a distinct mark.
       WHAT THIS COSTS: the accents are 60deg apart by Material's harmony rule, where the
       old pair was 148deg. Human and AI are now closer in hue than they have ever been,
       and the glyph inside each ring — not the colour — is what a reader who cannot tell
       them apart has left. Do not remove the glyphs. */
    --gn-c3-human: var(--gn-accent);
    --gn-c3-ai: var(--gn-accent-2);
}

/* ── --gn-accent-2, and where its twelve values came from ────────────────────
   Material's rule, not a taste call. Their generator (material-color-utilities,
   core_palette.ts) builds the contrasting accent as hue + 60° off the primary at half its
   chroma, and M2 said the same thing in words: a secondary "can be analogous or
   complementary… but should not simply be a light or dark variation of your primary" and
   "should be applied sparingly as an accent". That last line is why --gn-link could never
   be it — --gn-link IS a light variation of --gn-accent in all twelve palettes, and in
   terminal-light it is literally the same hex.

   Two documented departures, both forced by things Material doesn't have to deal with:

   1. THE HUE STEP IS 60° BUT NOT ALWAYS THE FIRST ONE. Material reserves exactly one hue
      (error, HCT 25). We reserve six — accent, code, success, danger, warning, info — so
      +60 often lands on a hue that already means something here: +60 off terminal's green
      is cyan, which is --gn-info, and +60 off console's magenta is orange, which is
      --gn-code. So we keep Material's 60° step and skip occupied stops, taking the first
      that clears every reserved hue by 30°. Most palettes land on +120; blueprint and noir
      take Material's +60 unchanged; ember needs +240.

   2. CHROMA IS THE PRIMARY'S, NOT HALF. Material halves it — but only after flooring the
      primary at HCT chroma 48. Our accents are authored below that floor, so halving them
      produced greys (#7b97b3, #b6a6c4) rather than accents. Keeping the primary's own
      chroma is what preserves each palette's character: noir's stays restrained, terminal's
      stays electric.

   Tone is the free variable, because Material fixes a tone only by owning the background,
   and we have twelve. Each value is the lightness that DELIVERS the most chroma (asking
   green's chroma of a blue at green's lightness gamut-clips to near-white, which reads as
   disabled) while clearing 4.5:1 on --gn-bg, --gn-surface AND --gn-surface-2, and never
   exceeding its own primary's contrast by more than a quarter — a colour used rarely must
   not outshine the one used everywhere.

   To retune a palette or author a thirteenth, re-run the derivation rather than eyeballing
   a hex; the constraint set above is what makes these twelve mutually consistent. */

/* ── 1a. The light ground — ADR-044 §2's paper sibling of the default ────────
   Colours only: the scale, spacing, radii and floors above are ground-agnostic
   and are NOT repeated here. Two of the twelve palettes; S2.2 authors the rest.

   Selects on data-scheme (the resolved ground), never data-mode (the preference) —
   `auto` is a preference and can never be a ground. */
:root[data-scheme="light"] {
    color-scheme: light;
    --gn-bg: #f5f3ee;
    --gn-surface: #ffffff;
    --gn-surface-2: #e9e6df;
    --gn-border: #d8d3c8;
    --gn-border-strong: #7d7869;
    --gn-text: #20242a;
    --gn-text-muted: #585f66;
    --gn-accent: #0d6d66;
    --gn-accent-hover: #0a5852;
    --gn-accent-2: #72598b;
    --gn-on-accent-2: #ffffff;
    --gn-on-accent: #ffffff;        /* fills are deep on paper, so the ink flips to white */
    --gn-fill-shade: black;         /* …and hover/active darken rather than lighten */
    --gn-link: #0b625c;
    --gn-link-hover: #084b46;
    --gn-success: #1c6b46;
    --gn-danger: #a92620;
    --gn-warning: #7a5406;
    --gn-info: #0f5f79;
    --gn-code: #9a4a12;
    --gn-focus-ring: rgba(13, 109, 102, .28);
    --gn-shadow: 0 1px 2px rgba(31, 29, 24, .06), 0 4px 12px rgba(31, 29, 24, .07);
    --gn-sidebar-bg: #e9e6df;
    --gn-sidebar-text: #2f353b;
    --bs-body-bg-rgb: 245, 243, 238;
    --bs-body-color-rgb: 32, 36, 42;
    --bs-danger-rgb: 169, 38, 32;

    /* The word-role colours, re-toned for a light ground (S7.0 item C2, owner 2026-08-13).
       ADR-008's rule holds — a colour still means ONE role — but it now means it per GROUND:
       the dark set in §1 was measured at 1.05–2.74:1 on the light grounds (adjective was
       white-on-white) and no single hue can serve both. Keyed on data-scheme, NOT per
       palette (the owner's call: light/dark is the render state, twelve styles are not),
       so a light-ground palette worn in the dark slot still gets the dark set — a corner a
       reader can only reach on purpose. Derived in OKLCH: same hue, L walked down until
       ≥4.5:1 against every light palette's bg AND surface; both game engines and the
       power-up chips read these off computed style at level build, so no engine change.
       Verified by the same sweep that found the failure — re-run it before touching a hex. */
    --gn-word-adjective: #5d7081;
    --gn-word-contraction: #507478;
    --gn-word-jargon: #d71551;
    --gn-word-noun: #1a6cd1;
    --gn-word-verb: #ba4a0a;
    --gn-word-reserved: #9046de;
    --gn-word-hedge: #71669a;
}

/* ADR-042 §8: below a 560px viewport height the chrome shrinks, so a short
   viewport is not mostly bar and strip. A height query — the min-width-only rule
   governs width. */
@media (max-height: 560px) {
    :root {
        --gn-bar-h: 48px;
        --gn-strip-h: 48px;
    }
}

/* ── 1b. The palette catalogue (ADR-003 mechanism, ADR-044 §2 content) ──────
   data-theme on <html> comes from the user's theme_mode claim (App.razor).
   Twelve palettes in six families, each family a dark-ground and a light-ground
   member; the keys are seeded in BlogContext and the colours are authored HERE
   (ADR-044 §7 — the admin surface edits metadata only, never hues).
   Every block declares the FULL token set, colours and color-scheme alike. Anything a
   block omits leaks through from the ground underneath, which is how a dark palette
   parked in the light slot ends up with color-scheme: light.

   EACH BLOCK CARRIES A SECOND SELECTOR, [data-gn-palette="…"], so a palette can be worn
   by ONE ELEMENT instead of by the page. That is what lets /Account/Manage/Appearance
   show twelve swatch rows without a single hex leaving this file — the swatches are
   var(--gn-bg) and friends, resolved against whichever block the row opted into. The
   root selector is (0,2,0) and this one is (0,1,0); specificity is per-selector in a
   list, so adding it changes nothing about how the page-level palette resolves.

   Every value below was checked by script, not by eye: text and semantic hues ≥4.5:1
   on bg AND surface AND surface-2, --gn-border-strong ≥3:1 (WCAG 1.4.11), and
   --gn-on-accent ≥4.5:1 on every filled control including Bootstrap's 15%/20%
   hover and active shades in the --gn-fill-shade direction.

   MUST STAY BELOW §1a. :root[data-theme=…] and :root[data-scheme=…] are both
   (0,2,0); only source order decides, and a chosen palette has to beat the
   ground it is being shown on (ADR-044 §2 — all twelve are eligible for either
   slot, so a dark palette in the light slot is a legal choice, not a bug). */
/* The default, and the same values as the dark ground in §1 — repeated here in full
   because a palette block must declare the whole set (a partial block leaks the OTHER ground
   through when it is used in the opposite slot). */
:root[data-theme="verdigris-dark"],
[data-gn-palette="verdigris-dark"] {
    color-scheme: dark;
    --gn-bg: #15181b;
    --gn-surface: #1d2125;
    --gn-surface-2: #262c31;
    --gn-border: #333b41;
    --gn-border-strong: #6c7883;
    --gn-text: #e2e7ea;
    --gn-text-muted: #98a3ab;
    --gn-accent: #2fa89f;
    --gn-accent-hover: #3ec4ba;
    --gn-accent-2: #a784c7;
    --gn-on-accent-2: #04120f;
    --gn-on-accent: #04120f;
    --gn-fill-shade: white;
    --gn-link: #5accc2;
    --gn-link-hover: #84dcd4;
    --gn-success: #54b988;
    --gn-danger: #f0736f;
    --gn-warning: #e2b04a;
    --gn-info: #5cb8dc;
    --gn-code: #e9a06d;
    --gn-focus-ring: rgba(47, 168, 159, .30);
    --gn-shadow: none;
    --gn-sidebar-bg: #1d2125;
    --gn-sidebar-text: #c8d0d5;
    --bs-body-bg-rgb: 21, 24, 27;
    --bs-body-color-rgb: 226, 231, 234;
    --bs-danger-rgb: 240, 115, 111;
}

/* The paper sibling, and the same values as the light ground in §1a, repeated for the
   same reason. */
:root[data-theme="verdigris-light"],
[data-gn-palette="verdigris-light"] {
    color-scheme: light;
    --gn-bg: #f5f3ee;
    --gn-surface: #ffffff;
    --gn-surface-2: #e9e6df;
    --gn-border: #d8d3c8;
    --gn-border-strong: #7d7869;
    --gn-text: #20242a;
    --gn-text-muted: #585f66;
    --gn-accent: #0d6d66;
    --gn-accent-hover: #0a5852;
    --gn-accent-2: #72598b;
    --gn-on-accent-2: #ffffff;
    --gn-on-accent: #ffffff;
    --gn-fill-shade: black;
    --gn-link: #0b625c;
    --gn-link-hover: #084b46;
    --gn-success: #1c6b46;
    --gn-danger: #a92620;
    --gn-warning: #7a5406;
    --gn-info: #0f5f79;
    --gn-code: #9a4a12;
    --gn-focus-ring: rgba(13, 109, 102, .30);
    --gn-shadow: 0 1px 2px rgba(31, 29, 24, .06), 0 4px 12px rgba(31, 29, 24, .07);
    --gn-sidebar-bg: #e9e6df;
    --gn-sidebar-text: #2f353b;
    --bs-body-bg-rgb: 245, 243, 238;
    --bs-body-color-rgb: 32, 36, 42;
    --bs-danger-rgb: 169, 38, 32;
}

/* Near black with a magenta cursor. */
:root[data-theme="console-dark"],
[data-gn-palette="console-dark"] {
    color-scheme: dark;
    --gn-bg: #0c0c11;
    --gn-surface: #14141c;
    --gn-surface-2: #1d1d27;
    --gn-border: #2e2e3c;
    --gn-border-strong: #71718a;
    --gn-text: #e9e7f2;
    --gn-text-muted: #a4a1b8;
    --gn-accent: #ff5fb0;
    --gn-accent-hover: #ff8cc8;
    --gn-accent-2: #aeb408;
    --gn-on-accent-2: #14000c;
    --gn-on-accent: #14000c;
    --gn-fill-shade: white;
    --gn-link: #ff7ec0;
    --gn-link-hover: #ffa8d5;
    --gn-success: #52c48e;
    --gn-danger: #ff6f75;
    --gn-warning: #e8b24c;
    --gn-info: #6cbcf0;
    --gn-code: #e9a06d;
    --gn-focus-ring: rgba(255, 95, 176, .30);
    --gn-shadow: none;
    --gn-sidebar-bg: #14141c;
    --gn-sidebar-text: #cdc9dd;
    --bs-body-bg-rgb: 12, 12, 17;
    --bs-body-color-rgb: 233, 231, 242;
    --bs-danger-rgb: 255, 111, 117;
}

/* The console palette with the lights on: the magenta goes deep enough to be read as ink. */
:root[data-theme="console-light"],
[data-gn-palette="console-light"] {
    color-scheme: light;
    --gn-bg: #f7f5f9;
    --gn-surface: #ffffff;
    --gn-surface-2: #eceaf1;
    --gn-border: #d3cddd;
    --gn-border-strong: #7c7590;
    --gn-text: #1c1a22;
    --gn-text-muted: #57536a;
    --gn-accent: #a3005f;
    --gn-accent-hover: #820049;
    --gn-accent-2: #677001;
    --gn-on-accent-2: #ffffff;
    --gn-on-accent: #ffffff;
    --gn-fill-shade: black;
    --gn-link: #9c005b;
    --gn-link-hover: #7a0047;
    --gn-success: #1c6b46;
    --gn-danger: #b02020;
    --gn-warning: #7a5406;
    --gn-info: #0f5f79;
    --gn-code: #9a4a12;
    --gn-focus-ring: rgba(163, 0, 95, .30);
    --gn-shadow: 0 1px 2px rgba(31, 29, 24, .06), 0 4px 12px rgba(31, 29, 24, .07);
    --gn-sidebar-bg: #eceaf1;
    --gn-sidebar-text: #2c2937;
    --bs-body-bg-rgb: 247, 245, 249;
    --bs-body-color-rgb: 28, 26, 34;
    --bs-danger-rgb: 176, 32, 32;
}

/* Predates the catalogue and keeps its key — live theme_mode claims hold this string.
   Ratios re-checked here (ADR-044 §2), and the four semantic hues it never had are authored:
   before this block it inherited red/amber/blue from the ground, on a screen of phosphor green.
   --gn-font is deliberately NOT set: after ADR-044 §3 the font is the reader's, not the palette's,
   and S2.3 makes a mono face the site default anyway. */
:root[data-theme="terminal-green"],
[data-gn-palette="terminal-green"] {
    color-scheme: dark;
    --gn-bg: #0a0f0a;
    --gn-surface: #101810;
    --gn-surface-2: #162116;
    --gn-border: #1f4d1f;
    --gn-border-strong: #3f9e3f;
    --gn-text: #33ff33;
    --gn-text-muted: #23c223;
    --gn-accent: #33ff33;
    --gn-accent-hover: #66ff66;
    --gn-accent-2: #4481ff;
    --gn-on-accent-2: #000000;
    --gn-on-accent: #000000;
    --gn-fill-shade: white;
    --gn-link: #66ff66;
    --gn-link-hover: #99ff99;
    --gn-success: #4dff8f;
    --gn-danger: #ff6b6b;
    --gn-warning: #ffd24d;
    --gn-info: #4de0ff;
    --gn-code: #ffb86b;
    --gn-focus-ring: rgba(51, 255, 51, .30);
    --gn-shadow: none;
    --gn-sidebar-bg: #071207;
    --gn-sidebar-text: #33ff33;
    --bs-body-bg-rgb: 10, 15, 10;
    --bs-body-color-rgb: 51, 255, 51;
    --bs-danger-rgb: 255, 107, 107;
}

/* Phosphor ink on a pale ground. The green has to darken a long way to be readable
   as text rather than glow. */
:root[data-theme="terminal-light"],
[data-gn-palette="terminal-light"] {
    color-scheme: light;
    --gn-bg: #eef2ec;
    --gn-surface: #ffffff;
    --gn-surface-2: #e2e8df;
    --gn-border: #c9d2c5;
    --gn-border-strong: #6f7d6b;
    --gn-text: #10240f;
    --gn-text-muted: #46543f;
    --gn-accent: #146b14;
    --gn-accent-hover: #0f5210;
    --gn-accent-2: #325bb1;
    --gn-on-accent-2: #ffffff;
    --gn-on-accent: #ffffff;
    --gn-fill-shade: black;
    --gn-link: #146b14;
    --gn-link-hover: #0f5210;
    --gn-success: #1c6b46;
    --gn-danger: #a92620;
    --gn-warning: #6d4c05;
    --gn-info: #0f5f79;
    --gn-code: #8a4413;
    --gn-focus-ring: rgba(20, 107, 20, .30);
    --gn-shadow: 0 1px 2px rgba(31, 29, 24, .06), 0 4px 12px rgba(31, 29, 24, .07);
    --gn-sidebar-bg: #e2e8df;
    --gn-sidebar-text: #1c2f1a;
    --bs-body-bg-rgb: 238, 242, 236;
    --bs-body-color-rgb: 16, 36, 15;
    --bs-danger-rgb: 169, 38, 32;
}

/* Cyanotype blue under white rules. */
:root[data-theme="blueprint-dark"],
[data-gn-palette="blueprint-dark"] {
    color-scheme: dark;
    --gn-bg: #0b1d33;
    --gn-surface: #12283f;
    --gn-surface-2: #1a334c;
    --gn-border: #27455f;
    --gn-border-strong: #6b90ad;
    --gn-text: #e6eef5;
    --gn-text-muted: #9db3c6;
    --gn-accent: #6ecbff;
    --gn-accent-hover: #9adcff;
    --gn-accent-2: #c0abfd;
    --gn-on-accent-2: #041220;
    --gn-on-accent: #041220;
    --gn-fill-shade: white;
    --gn-link: #7fd2ff;
    --gn-link-hover: #a6e2ff;
    --gn-success: #56c68f;
    --gn-danger: #ff7b7b;
    --gn-warning: #e9b552;
    --gn-info: #79d8e8;
    --gn-code: #f0a878;
    --gn-focus-ring: rgba(110, 203, 255, .30);
    --gn-shadow: none;
    --gn-sidebar-bg: #12283f;
    --gn-sidebar-text: #cfe0ee;
    --bs-body-bg-rgb: 11, 29, 51;
    --bs-body-color-rgb: 230, 238, 245;
    --bs-danger-rgb: 255, 123, 123;
}

/* Drafting stock, blue rules, graphite type. */
:root[data-theme="blueprint-light"],
[data-gn-palette="blueprint-light"] {
    color-scheme: light;
    --gn-bg: #eef1f5;
    --gn-surface: #ffffff;
    --gn-surface-2: #e2e7ee;
    --gn-border: #cdd5df;
    --gn-border-strong: #6f7a88;
    --gn-text: #1e242b;
    --gn-text-muted: #545c66;
    --gn-accent: #1a5490;
    --gn-accent-hover: #133f6e;
    --gn-accent-2: #7d5095;
    --gn-on-accent-2: #ffffff;
    --gn-on-accent: #ffffff;
    --gn-fill-shade: black;
    --gn-link: #17558f;
    --gn-link-hover: #10406d;
    --gn-success: #1c6b46;
    --gn-danger: #a92620;
    --gn-warning: #74500a;
    --gn-info: #0f5f79;
    --gn-code: #92440f;
    --gn-focus-ring: rgba(26, 84, 144, .30);
    --gn-shadow: 0 1px 2px rgba(31, 29, 24, .06), 0 4px 12px rgba(31, 29, 24, .07);
    --gn-sidebar-bg: #e2e7ee;
    --gn-sidebar-text: #2b323a;
    --bs-body-bg-rgb: 238, 241, 245;
    --bs-body-color-rgb: 30, 36, 43;
    --bs-danger-rgb: 169, 38, 32;
}

/* Slate, smoke and a cold blue. */
:root[data-theme="noir-dark"],
[data-gn-palette="noir-dark"] {
    color-scheme: dark;
    --gn-bg: #121417;
    --gn-surface: #1a1d21;
    --gn-surface-2: #23272c;
    --gn-border: #31363d;
    --gn-border-strong: #6a727c;
    --gn-text: #dfe3e8;
    --gn-text-muted: #969da6;
    --gn-accent: #7fb2e6;
    --gn-accent-hover: #a3caf0;
    --gn-accent-2: #b694d1;
    --gn-on-accent-2: #08111c;
    --gn-on-accent: #08111c;
    --gn-fill-shade: white;
    --gn-link: #8dbcec;
    --gn-link-hover: #b0d3f4;
    --gn-success: #5cbb8c;
    --gn-danger: #ef7373;
    --gn-warning: #dfae4e;
    --gn-info: #64bcd9;
    --gn-code: #e0a17a;
    --gn-focus-ring: rgba(127, 178, 230, .30);
    --gn-shadow: none;
    --gn-sidebar-bg: #1a1d21;
    --gn-sidebar-text: #c5cbd2;
    --bs-body-bg-rgb: 18, 20, 23;
    --bs-body-color-rgb: 223, 227, 232;
    --bs-danger-rgb: 239, 115, 115;
}

/* Pale linen and deep indigo. */
:root[data-theme="noir-light"],
[data-gn-palette="noir-light"] {
    color-scheme: light;
    --gn-bg: #f4f2ee;
    --gn-surface: #fdfcfa;
    --gn-surface-2: #e8e5df;
    --gn-border: #d6d2ca;
    --gn-border-strong: #77726a;
    --gn-text: #1f2229;
    --gn-text-muted: #565a62;
    --gn-accent: #2f4478;
    --gn-accent-hover: #23335a;
    --gn-accent-2: #582c5a;
    --gn-on-accent-2: #ffffff;
    --gn-on-accent: #ffffff;
    --gn-fill-shade: black;
    --gn-link: #2c4074;
    --gn-link-hover: #1f2e55;
    --gn-success: #1c6b46;
    --gn-danger: #a92620;
    --gn-warning: #7a5406;
    --gn-info: #0f5f79;
    --gn-code: #9a4a12;
    --gn-focus-ring: rgba(47, 68, 120, .30);
    --gn-shadow: 0 1px 2px rgba(31, 29, 24, .06), 0 4px 12px rgba(31, 29, 24, .07);
    --gn-sidebar-bg: #e8e5df;
    --gn-sidebar-text: #2c3038;
    --bs-body-bg-rgb: 244, 242, 238;
    --bs-body-color-rgb: 31, 34, 41;
    --bs-danger-rgb: 169, 38, 32;
}

/* Warm dark browns around one live coal. --gn-code is pulled off the orange ramp so it
   stays tellable from the accent. */
:root[data-theme="ember-dark"],
[data-gn-palette="ember-dark"] {
    color-scheme: dark;
    --gn-bg: #17120f;
    --gn-surface: #1f1915;
    --gn-surface-2: #29211c;
    --gn-border: #3a2f27;
    --gn-border-strong: #8a7565;
    --gn-text: #efe6de;
    --gn-text-muted: #b2a294;
    --gn-accent: #ff8f4d;
    --gn-accent-hover: #ffae7d;
    --gn-accent-2: #9d8bf9;
    --gn-on-accent-2: #1a0c02;
    --gn-on-accent: #1a0c02;
    --gn-fill-shade: white;
    --gn-link: #ffa268;
    --gn-link-hover: #ffc19a;
    --gn-success: #64c08d;
    --gn-danger: #ff7a72;
    --gn-warning: #e6b74f;
    --gn-info: #66bcda;
    --gn-code: #e8c07d;
    --gn-focus-ring: rgba(255, 143, 77, .30);
    --gn-shadow: none;
    --gn-sidebar-bg: #1f1915;
    --gn-sidebar-text: #d5c8bc;
    --bs-body-bg-rgb: 23, 18, 15;
    --bs-body-color-rgb: 239, 230, 222;
    --bs-danger-rgb: 255, 122, 114;
}

/* Warm neutrals and a clay accent. */
:root[data-theme="ember-light"],
[data-gn-palette="ember-light"] {
    color-scheme: light;
    --gn-bg: #f7f2e9;
    --gn-surface: #fffdf8;
    --gn-surface-2: #ece5d8;
    --gn-border: #dcd3c2;
    --gn-border-strong: #7e7462;
    --gn-text: #241f19;
    --gn-text-muted: #5c5449;
    --gn-accent: #9c4213;
    --gn-accent-hover: #7c340e;
    --gn-accent-2: #615bb2;
    --gn-on-accent-2: #ffffff;
    --gn-on-accent: #ffffff;
    --gn-fill-shade: black;
    --gn-link: #96400f;
    --gn-link-hover: #74300b;
    --gn-success: #1c6b46;
    --gn-danger: #a92620;
    --gn-warning: #6f4c05;
    --gn-info: #0f5f79;
    --gn-code: #7a4a12;
    --gn-focus-ring: rgba(156, 66, 19, .30);
    --gn-shadow: 0 1px 2px rgba(31, 29, 24, .06), 0 4px 12px rgba(31, 29, 24, .07);
    --gn-sidebar-bg: #ece5d8;
    --gn-sidebar-text: #302a22;
    --bs-body-bg-rgb: 247, 242, 233;
    --bs-body-color-rgb: 36, 31, 25;
    --bs-danger-rgb: 169, 38, 32;
}
/* hot-dog-stand was here. Retired 2026-08-09 (ADR-044 §6): the goal is letting readers read
   comfortably, not offering hostility as a novelty. Its ThemeMode row is DEACTIVATED rather than
   deleted, so live users holding the key fall back to the default with no user-database migration.
   Do not reintroduce the block — a stored key with no active row is the intended dead end. */

/* ── 1c. The font list (ADR-044 §4) ─────────────────────────────────────────
   data-font on <html> comes from the reader's choice (App.razor). The list is short on
   purpose: every self-hosted face is bytes on the wire.

   PER-CHOICE LOADING IS NOT A MECHANISM, IT IS THE ABSENCE OF ONE. A browser fetches an
   @font-face file only when a rule actually renders text in that family, and exactly one
   --gn-font wins per document — so declaring all four families below costs nothing to a
   reader who picked none of them. Do not "optimise" this into per-choice <link>s or a
   conditional loader; that would make it worse and slower.

   ADR-044 §4's finding is that the evidence is about SPACING, not typefaces — the
   accessibility work is the type scale, --gn-lh and --gn-measure in §1, not this section.
   The honest labelling that goes with each entry lives in Data/FontChoices.cs, next to the
   keys, because a label that drifts from its font is worse than no label.

   Files are woff2 only (every browser that runs this site supports it) and named by US, not
   by upstream — see wwwroot/fonts/README.md. font-display: swap so text is readable in the
   fallback while the file arrives. No real italics: browsers synthesise a passable oblique
   and a second file per family is not worth the bytes for a preference feature. */
@font-face {
    font-family: "Cascadia Mono";
    src: url("fonts/cascadia-mono.woff2") format("woff2");
    font-weight: 200 700;   /* variable */
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Atkinson Hyperlegible";
    src: url("fonts/atkinson-hyperlegible.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Atkinson Hyperlegible";
    src: url("fonts/atkinson-hyperlegible-bold.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Lexend";
    src: url("fonts/lexend.woff2") format("woff2");
    font-weight: 100 900;   /* variable */
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "OpenDyslexic";
    src: url("fonts/opendyslexic.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "OpenDyslexic";
    src: url("fonts/opendyslexic-bold.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Source Sans 3";
    src: url("fonts/source-sans-3.woff2") format("woff2");
    font-weight: 200 900;   /* variable */
    font-style: normal;
    font-display: swap;
}

/* One block per FontChoices key. (0,2,0), so these beat §1's default whatever the order.
   Every stack keeps a system fallback: if a woff2 is missing the page is plain, not broken.

   EACH BLOCK CARRIES A SECOND SELECTOR, [data-gn-font="…"], for exactly the reason §1b's
   palettes carry [data-gn-palette]: so a choice can be worn by ONE ELEMENT instead of by
   the page, and /styleguide can show all eight specimens at once (row S4.5). The root
   selector stays (0,2,0) and the element one is (0,1,0) — specificity is per-selector in
   a list, so adding it changes nothing about how the page-level font resolves.
   Add a font and it needs BOTH selectors, or it is invisible to the preview.

   Cascadia has no block: it is the :root default in §1, and a [data-gn-font="cascadia"]
   element inherits it already. */
:root[data-font="system-sans"], [data-gn-font="system-sans"] { --gn-font: var(--bs-font-sans-serif); }
/* Bootstrap has no serif variable — this is the only stack written out by hand. */
:root[data-font="system-serif"], [data-gn-font="system-serif"] { --gn-font: Georgia, Cambria, "Times New Roman", serif; }
:root[data-font="system-mono"], [data-gn-font="system-mono"] { --gn-font: var(--bs-font-monospace); }
:root[data-font="atkinson"], [data-gn-font="atkinson"] { --gn-font: "Atkinson Hyperlegible", var(--bs-font-sans-serif); }
:root[data-font="lexend"], [data-gn-font="lexend"] { --gn-font: "Lexend", var(--bs-font-sans-serif); }
:root[data-font="opendyslexic"], [data-gn-font="opendyslexic"] { --gn-font: "OpenDyslexic", var(--bs-font-sans-serif); }
:root[data-font="source-sans"], [data-gn-font="source-sans"] { --gn-font: "Source Sans 3", var(--bs-font-sans-serif); }

/* --gn-font is a token, not a font-family rule: nothing reads it unless something says
   `font-family: var(--gn-font)`. On the page that is --bs-body-font-family in §2, which
   `body` picks up — but a [data-gn-font] specimen is a DIV, and it inherits whatever body
   already resolved. This is the one line that makes the attribute do anything on its own. */
[data-gn-font] {
    font-family: var(--gn-font);
}

/* ── 1d. The size and measure dials (ADR-044 §3/§4) ──────────────────────────
   Where ADR-044 §4's accessibility budget actually gets spent: the evidence is about
   spacing, not typefaces, so these two matter more than the list above them.

   THE PERCENTAGES AND THE ch VALUES LIVE HERE AND NOWHERE ELSE. Data/AppearanceChoices.cs
   holds the keys and the words; putting a number in both would be two places to disagree.

   data-size moves --gn-fs-root, which §1 declares as `font-size` on :root — so it moves
   EVERY step of the scale at once, headings included, because every step is a rem. That is
   also why no rule on this site may write a literal font-size: a literal resolves against
   nothing the reader can turn, and drops silently out of this control.

   A percentage, never a rem or a px: it resolves against the reader's own browser default,
   so "Default" honours someone who already set 20px in their browser instead of overriding
   them back down to 16. `md` therefore needs no block — it IS §1's 100%.

   No [data-gn-size] / [data-gn-measure] element twins, unlike §1b's palettes and §1c's
   fonts. Those exist so /styleguide can show twelve palettes at once; a size specimen is
   already the whole page, and a --gn-fs-root that resolved per element would mean rem
   meaning two different things in one document. */
:root[data-size="xs"] { --gn-fs-root: 80%; }
:root[data-size="sm"] { --gn-fs-root: 90%; }
:root[data-size="lg"] { --gn-fs-root: 115%; }
:root[data-size="xl"] { --gn-fs-root: 130%; }

/* ch, never px (ADR-042 §7): the line holds the same number of CHARACTERS whichever face
   §1c resolved, and a px width silently becomes a 95-character line on a narrow one. */
:root[data-measure="sm"] { --gn-measure: 56ch; }
:root[data-measure="lg"] { --gn-measure: 84ch; }

/* Selection controls, site-wide — M2's own list for the second hue ("selection controls
   like sliders and switches… progress bars"). One rule replaces every hand-set accent-color.

   On the ELEMENTS, deliberately, not on :root. accent-color: var(…) set at the root would
   compute to one colour there and inherit as that colour, so a [data-gn-palette] subtree
   could never show its own — which is exactly what the palette picker's rows need. Resolved
   per element, each one reads whichever palette it actually sits in. */
input[type="checkbox"],
input[type="radio"],
input[type="range"],
progress {
    accent-color: var(--gn-accent-2);
}

/* ── 2. Bootstrap 5.3 remaps — route Bootstrap through the tokens ─────────── */
:root {
    --bs-body-font-family: var(--gn-font);
    --bs-body-font-size: var(--gn-fs-md);
    --bs-body-line-height: var(--gn-lh);
    --bs-body-bg: var(--gn-bg);
    --bs-body-color: var(--gn-text);
    /* Bootstrap hardcodes #d63384 / #e685b5 here and nothing routed it through the
       tokens, so inline <code> stayed pink under every persona theme. */
    --bs-code-color: var(--gn-code);
    --bs-secondary-color: var(--gn-text-muted);
    --bs-secondary-bg: var(--gn-surface-2);
    --bs-tertiary-bg: var(--gn-surface);
    --bs-border-color: var(--gn-border);
    --bs-border-radius: var(--gn-radius);
    --bs-border-radius-sm: var(--gn-radius-sm);
    --bs-border-radius-lg: var(--gn-radius-lg);
    --bs-link-color: var(--gn-link);
    --bs-link-hover-color: var(--gn-link-hover);
    --bs-focus-ring-color: var(--gn-focus-ring);
    --bs-box-shadow: var(--gn-shadow);
    --bs-form-valid-color: var(--gn-success);
    --bs-form-valid-border-color: var(--gn-success);
    --bs-form-invalid-color: var(--gn-danger);
    --bs-form-invalid-border-color: var(--gn-danger);
}

/* 5.3 anchors read RGB-triplet vars (rgba(var(--bs-link-color-rgb),…));
   route them through the hex tokens instead so only hex needs maintaining.
   Costs the (unused) link-opacity utilities. */
a {
    color: var(--bs-link-color);
}

a:hover {
    color: var(--bs-link-hover-color);
}

/* Bootstrap has no heading-font variable. Sizes come off the scale rather than
   Bootstrap's own ladder, so a type-scale step moves the headings too — otherwise
   the reader turns the dial and only the body text answers.
   h1/h2 keep a fluid middle term because Bootstrap's already were; the clamp
   BOUNDS are scale tokens, so the user's step still moves both ends. Anything
   fluid beyond these two is parked (PLAN Parking Lot). */
h1, h2, h3, h4, h5, h6 {
    font-family: var(--gn-font-heading);
    line-height: var(--gn-lh-tight);
}

h1 { font-size: clamp(var(--gn-fs-3xl), 1.375rem + 1.5vw, var(--gn-fs-4xl)); }
h2 { font-size: clamp(var(--gn-fs-2xl), 1.325rem + .9vw, var(--gn-fs-3xl)); }
h3 { font-size: var(--gn-fs-2xl); }
h4 { font-size: var(--gn-fs-xl); }
h5 { font-size: var(--gn-fs-lg); }
h6 { font-size: var(--gn-fs-md); }

/* Same problem as the headings, one class further down: Bootstrap's .lead is a bare
   `font-size: 1.25rem; font-weight: 300`, so a reader turning the type dial moved every
   paragraph on the page EXCEPT the standfirst. On the scale it becomes --gn-fs-lg. The
   weight goes back to 400 as well — 300 is a face this site does not load (Cascadia Mono
   ships one weight), so it was already synthesising or silently falling back. Row S4.5. */
.lead {
    font-size: var(--gn-fs-lg);
    font-weight: 400;
}

/* The ground, the ink and the face — on OUR side of the seam (S7.0 item B1, 2026-08-13).
   These three used to reach the page only through §2's remaps feeding Bootstrap's reboot
   `body` rule, which put the site's most identity-defining values on the wrong side of
   ADR-043 §6's test: pull the vendored sheet and the page rendered transparent Times New
   Roman. The remaps above stay — Bootstrap's own plumbing still reads them — this rule is
   what makes the identity survive without them. margin: 0 is here for the same reason;
   it is reboot's, and without it the seam test grows an 8px halo. */
body {
    margin: 0;
    background-color: var(--gn-bg);
    color: var(--gn-text);
    font-family: var(--gn-font);
    font-size: var(--gn-fs-md);
    line-height: var(--gn-lh);
}

/* ── Responsive floors that are genuinely global (ADR-042 §8) ────────────────
   Only the two that can be stated once for the whole site live here. The 44px
   touch target is a --gn-touch number for each component to honour, not a blanket
   min-height: growing every control in one rule would resize the entire site from
   a token row, and ADR-042 §8's own wording is "grow the target, keep the ink small". */

/* iOS zooms the page when a focused input computes under 16px and never zooms back.
   max() so the floor survives a reader dialling the type scale down. */
[data-gn-input="coarse"] :is(input, select, textarea) {
    font-size: max(16px, var(--gn-fs-md));
}

/* WCAG 1.4.11: a control's boundary needs 3:1, which a quiet card separator does
   not. --gn-border stays quiet for cards and rules; controls take the strong one. */
.form-control, .form-select, .form-check-input, .form-range {
    border-color: var(--gn-border-strong);
}

/* ── Buttons — OURS (ADR-043 §2, migrated by S7.0 item B2, 2026-08-13) ───────
   The .btn-* class NAMES stay Bootstrap's — ~150 call sites keep working and renaming
   them buys no reader anything. The RULES are ours: styled directly off the tokens,
   winning over the vendored sheet purely by source order (same specificity, later
   file), keeping the exact geometry Bootstrap gave them so nothing moved the day they
   landed. Before this, the blocks here only fed --bs-btn-* variables into rules that
   lived in bootstrap.min.css, so ADR-043 §6's test failed on every button (the fills
   went grey). Now: pull the vendored sheet and a button keeps its look.

   Two deliberate changes from Bootstrap's originals:
     - sizes sit ON THE SCALE (--gn-fs-md/sm/lg), not literal rems, so button text
       follows the reader's type dial like everything else (ADR-044 §4);
     - there are NO .btn-outline-* rules. Filled, never transparent (owner rule,
       2026-07-22) — the four outline remap blocks were deleted here, not migrated
       (S7.0 item B3). An outline class in markup now renders as a plain .btn.

   Hover/active shades reproduce the remap-era math: 15%/20% toward --gn-fill-shade,
   the same mix palette-contrast.py walks when it checks --gn-on-accent (its shade()
   is this color-mix in Python). gn-cta is always used as `btn gn-cta`. */
.btn {
    display: inline-block;
    padding: .375rem .75rem;
    font-family: var(--gn-font);
    font-size: var(--gn-fs-md);
    font-weight: 400;
    line-height: var(--gn-lh);
    text-align: center;
    text-decoration: none;
    vertical-align: middle;
    cursor: pointer;
    user-select: none;
    border: 1px solid transparent;
    border-radius: var(--gn-radius);
    background-color: transparent;
    color: var(--gn-text);
    transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out;
}

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

.btn:focus-visible {
    outline: 0;
    box-shadow: 0 0 0 .25rem var(--gn-focus-ring);
}

.btn:disabled, .btn.disabled, fieldset:disabled .btn {
    pointer-events: none;
    opacity: .65;
}

.btn-sm {
    padding: .25rem .5rem;
    font-size: var(--gn-fs-sm);
    border-radius: var(--gn-radius-sm);
}

.btn-lg {
    padding: .5rem 1rem;
    font-size: var(--gn-fs-lg);
    border-radius: var(--gn-radius-lg);
}

.btn-primary, .gn-cta {
    color: var(--gn-on-accent);
    background-color: var(--gn-accent);
    border-color: var(--gn-accent);
}

.btn-primary:hover, .gn-cta:hover,
.btn-primary:active, .gn-cta:active {
    color: var(--gn-on-accent);
    background-color: var(--gn-accent-hover);
    border-color: var(--gn-accent-hover);
}

.btn-secondary {
    color: var(--gn-text);
    background-color: var(--gn-surface-2);
    border-color: var(--gn-border);
}

.btn-secondary:hover, .btn-secondary:active {
    color: var(--gn-text);
    background-color: var(--gn-border);
    border-color: var(--gn-border);
}

.btn-danger {
    color: var(--gn-on-accent);
    background-color: var(--gn-danger);
    border-color: var(--gn-danger);
}

.btn-danger:hover {
    color: var(--gn-on-accent);
    background-color: color-mix(in srgb, var(--gn-danger) 85%, var(--gn-fill-shade));
    border-color: color-mix(in srgb, var(--gn-danger) 85%, var(--gn-fill-shade));
}

.btn-danger:active {
    color: var(--gn-on-accent);
    background-color: color-mix(in srgb, var(--gn-danger) 80%, var(--gn-fill-shade));
    border-color: color-mix(in srgb, var(--gn-danger) 80%, var(--gn-fill-shade));
}

.btn-success {
    color: var(--gn-on-accent);
    background-color: var(--gn-success);
    border-color: var(--gn-success);
}

.btn-success:hover {
    color: var(--gn-on-accent);
    background-color: color-mix(in srgb, var(--gn-success) 85%, var(--gn-fill-shade));
    border-color: color-mix(in srgb, var(--gn-success) 85%, var(--gn-fill-shade));
}

.btn-success:active {
    color: var(--gn-on-accent);
    background-color: color-mix(in srgb, var(--gn-success) 80%, var(--gn-fill-shade));
    border-color: color-mix(in srgb, var(--gn-success) 80%, var(--gn-fill-shade));
}

/* Ink comes off --gn-on-accent like every other fill, so warning needs no special
   case: the dark ground already resolves it to dark ink. */
.btn-warning {
    color: var(--gn-on-accent);
    background-color: var(--gn-warning);
    border-color: var(--gn-warning);
}

.btn-warning:hover {
    color: var(--gn-on-accent);
    background-color: color-mix(in srgb, var(--gn-warning) 85%, var(--gn-fill-shade));
    border-color: color-mix(in srgb, var(--gn-warning) 85%, var(--gn-fill-shade));
}

.btn-warning:active {
    color: var(--gn-on-accent);
    background-color: color-mix(in srgb, var(--gn-warning) 80%, var(--gn-fill-shade));
    border-color: color-mix(in srgb, var(--gn-warning) 80%, var(--gn-fill-shade));
}

/* The one sanctioned non-filled variant: an inline text action wearing button bones. */
.btn-link {
    color: var(--gn-link);
    text-decoration: underline;
    background-color: transparent;
    border-color: transparent;
}

.btn-link:hover, .btn-link:active {
    color: var(--gn-link-hover);
    background-color: transparent;
    border-color: transparent;
}

.nav-pills {
    --bs-nav-pills-link-active-color: var(--gn-on-accent);
    --bs-nav-pills-link-active-bg: var(--gn-accent);
}

/* Bootstrap hardcodes these focus/checked colors — no variable exists. */
.form-control:focus, .form-select:focus, .form-check-input:focus {
    border-color: var(--gn-accent);
    outline: 0;
    box-shadow: 0 0 0 .25rem var(--gn-focus-ring);
}

.form-check-input:checked {
    background-color: var(--gn-accent);
    border-color: var(--gn-accent);
}

/* ── 3a. The shell frame — ADR-042 (bar · drawer · strip) ────────────────────
   Global, not scoped .razor.css, for three reasons that each rule out scoping on
   their own: the classes are `gn-*` (this file's job by the css-tokens contract);
   the drawer is driven by `#gn-nav:checked ~ …`, which crosses a component
   boundary the moment the drawer's contents are their own component; and `body`
   / `html` cannot be reached by scoped CSS at all — Blazor rewrites the selector
   to `body[b-xxxxx]`, which matches nothing.

   THE Z-STACK, DECIDED ONCE (ADR-042 Consequences). Nothing on the site may
   invent a new layer without changing this comment:

      1000   chrome: .gn-bar, .gn-strip
      1010   game canvas + overlay   (game.js #gn-game-overlay; its handle prompt at 1012)
             — ABOVE the chrome since 2026-08-13 (owner, S7.0 close-out): the ship flies
             over the strip, not under it. Chrome is inert while playing and the canvas is
             pointer-events:none, so the swap steals no clicks in either state. 900 is
             retired, not reassigned.
      1050   .gn-scrim
      1100   .gn-drawer
      1150   #gn-win — the cleared-the-run screen. Over the strip on purpose:
             it ends the run, so the readout behind it is finished with
      1150   .gn-toast — same reason, same clearance. The two never co-exist:
             a toast follows a form post, and a form post ends a run
      1200   .gn-skip, #blazor-error-ui — always reachable, over everything

   .gn-dafuq-panel is NOT on this list and does not need to be: a shown [popover] is in the
   browser's TOP LAYER, which sits above every stacking context here regardless of z-index.
   Row S3.8 added it without touching a number — the same is true of anything else that ever
   uses the popover API, which is the reason to prefer it.

   1090 used to hold the Bootstrap toast container. Row S4.7 retired it — the trophy earn
   announcement is a row in the document now, at no layer at all. Bootstrap's own .modal-backdrop
   (1050) and .toast-container (1090) still exist in the vendored CSS and DRIP's nine compiled
   modals still raise a backdrop on DRIP routes, which ADR-043 §4 states plainly rather than
   pretends away; nothing this repo authors goes there any more.

   The bar and the strip are opaque (--gn-surface), never translucent, so neither
   needs a gradient to read as chrome over the page. */

body {
    padding-top: var(--gn-bar-h);
    padding-bottom: calc(var(--gn-strip-h) + env(safe-area-inset-bottom));
}

/* WCAG 2.4.11 Focus Not Obscured: a focused element must never land under fixed
   chrome. FocusOnNavigate targets h1 on every navigation, so this is load-bearing,
   not a nicety. */
html {
    scroll-padding-top: calc(var(--gn-bar-h) + 8px);
    scroll-padding-bottom: calc(var(--gn-strip-h) + 8px);
    /* Reserve the scrollbar's gutter permanently. Without it, locking the scroll
       below hands those ~15px back to the layout and the whole page — bar, strip
       and prose — jumps sideways the instant the drawer opens. It also stops the
       same jump between a short page and a long one. */
    scrollbar-gutter: stable;
}

/* THE PAGE DOES NOT SCROLL BEHIND AN OPEN DRAWER (owner, 2026-08-08 — this
   reverses ADR-042 §3's "no body-scroll lock"). Two scrollbars side by side was
   the complaint and it is a fair one: shell.js already marks the bar, main and
   strip `inert` while the drawer is open, so the page behind it is not reachable
   — leaving it scrollable only offers a second bar that moves content nobody can
   touch. §3's other two refusals stand: still no aria-modal, still no focus trap.

   On the root, not on body: body's overflow only propagates to the viewport
   while the root's is `visible`, which is a condition another rule can quietly
   break. CSS-only, like the rest of the drawer — no JS to fall out of step with
   the checkbox. Known ceiling: iOS Safari ignores overflow:hidden here, and the
   fix for that is a position:fixed body plus scroll-position restore in JS —
   worth it only if the owner sees it on a real device. */
:root:has(.gn-nav-toggle:checked) {
    overflow: hidden;
}

.gn-skip {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 1200;
    background: var(--gn-accent);
    color: var(--gn-on-accent);
    padding: .7rem 1rem;
    font-weight: 700;
    text-decoration: none;
}

.gn-skip:focus {
    left: 0;
    color: var(--gn-on-accent);
}

/* The drawer's ONE focusable control (ADR-042 §3). Visually hidden but NOT
   display:none — it keeps its place in the tab order and Space still toggles it.
   aria-label pins its name: the Menu button and the X are further <label>s for
   this same checkbox, so without it their text would concatenate into one name. */
.gn-nav-toggle {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* ── The bar ─────────────────────────────────────────────────────────────── */
.gn-bar {
    position: fixed;
    z-index: 1000;
    inset: 0 0 auto 0;
    height: var(--gn-bar-h);
    display: flex;
    align-items: stretch;
    background: var(--gn-surface);
    border-bottom: 1px solid var(--gn-border);
}

.gn-bar-inner {
    width: 100%;
    max-width: 1360px;
    margin: 0 auto;
    padding: 0 var(--gn-gut);
    display: flex;
    align-items: stretch;
    gap: var(--gn-space-3);
    min-width: 0;
}

.gn-brand {
    display: flex;
    align-items: center;
    gap: var(--gn-space-2);
    font-size: var(--gn-fs-sm);
    font-weight: 700;
    letter-spacing: .04em;
    color: var(--gn-text);
    text-decoration: none;
    white-space: nowrap;
}

.gn-brand:hover {
    color: var(--gn-text);
}

/* ── The brand mark — ADR-045 §9 (row S3.7) ─────────────────────────────────
   The mark itself is inlined in MainLayout.razor and ships its own paint and its own
   hover choreography. Everything here is host business the generated file cannot know:
   how big it is, what ground it is standing on, and the two things the ADR adds.

   IDLE MOTION OFF. gn-logo.svg bobs, blinks and sways on an infinite loop. In a fixed bar
   on every page that is perpetual motion in the reader's peripheral vision, which is the
   same thing §3a already refused for the New Game+ pulse, and WCAG 2.2.2 is about exactly
   this. ADR-045 §9 asks for a HOVER animation; that is what is left. Switched off here
   rather than edited out of the markup so the generated copy stays paste-compatible — one
   deleted rule brings the whole loop back if the owner wants it.

   THE TRIGGER IS THE WHOLE LINK, hover and focus-visible alike (ADR-045 §9). The mark
   ships `.gnlogo:hover`, which is the mark itself — and the mark is 32px of a link that is
   mostly wordmark, so a reader hovering "GINGER NINJALYTICS" got nothing. Measured, not
   assumed. Keying both states off .gn-brand makes the target the thing that looks like the
   target, and gives the keyboard user what the mouse user sees, which is the half §9 asks
   for by name. (0,3,0) + an id, so these beat the file's own (0,2,0) + id hover rules on
   specificity rather than on source order — the inlined <style> is in the body, so source
   order would go the other way.

   INK ON THE GROUND IT IS ACTUALLY ON. The hood is near-black and the bar is
   --gn-surface, which is dark in six of the twelve palettes. The mark ships a .gn-on-dark
   variant for this, but a class would have to be chosen server-side and data-scheme can
   still flip client-side under mode=auto. light-dark() reads the element's own
   color-scheme, which every palette block already declares correctly (css-tokens.md), so
   it lands right in all twelve with no enumeration and nothing for the server to know.
   These two literals are the mark's own paint, not theme colour — they are the
   .gn-on-dark values verbatim, and they belong to the brand the way --gn-c3-* does.

   Transform and opacity only, never a layout property (ADR-045 §9): the bar is a fixed
   56px and anything that reflows it moves the whole page. Size is off the spacing scale
   for that reason — it is a fixed box the animation plays inside, not a growing one. */
.gn-brand .gnlogo {
    display: block;
    flex: none;
    width: var(--gn-space-4);
    height: var(--gn-space-4);
    --ink: light-dark(#14141c, #2e2e3f);
    --mask: light-dark(#282836, #41415a);
}

.gn-brand .gnlogo #gn-head,
.gn-brand .gnlogo #gn-eyes,
.gn-brand .gnlogo #gn-hairL,
.gn-brand .gnlogo #gn-hairR {
    animation: none;
}

.gn-brand:hover .gnlogo #gn-head,
.gn-brand:focus-visible .gnlogo #gn-head {
    transform: translateY(-2.5px) rotate(-4deg);
}

.gn-brand:hover .gnlogo #gn-eyes,
.gn-brand:focus-visible .gnlogo #gn-eyes {
    transform: scaleY(0.4) translateY(3px);
}

.gn-brand:hover .gnlogo #gn-hairL,
.gn-brand:hover .gnlogo #gn-hairR,
.gn-brand:focus-visible .gnlogo #gn-hairL,
.gn-brand:focus-visible .gnlogo #gn-hairR {
    transform: rotate(-5deg);
}

/* §2 rule 10. The mark's own file has this guard, but these rules are ours and sit above
   it on specificity, so they need their own — otherwise reduced-motion still gets the
   pose, just without the 0.18s ride to it. */
@media (prefers-reduced-motion: reduce) {
    .gn-brand:hover .gnlogo #gn-head,
    .gn-brand:hover .gnlogo #gn-eyes,
    .gn-brand:hover .gnlogo #gn-hairL,
    .gn-brand:hover .gnlogo #gn-hairR,
    .gn-brand:focus-visible .gnlogo #gn-head,
    .gn-brand:focus-visible .gnlogo #gn-eyes,
    .gn-brand:focus-visible .gnlogo #gn-hairL,
    .gn-brand:focus-visible .gnlogo #gn-hairR {
        transform: none;
    }
}

.gn-brand small {
    display: none;   /* returns at md */
    font-size: var(--gn-fs-xs);
    letter-spacing: .1em;
    color: var(--gn-text-muted);
    border-left: 1px solid var(--gn-border);
    padding-left: var(--gn-space-2);
}

/* Combo nav (ADR-042 §2): primary links stay VISIBLE from lg, and that is what
   makes a right-hand hidden drawer acceptable rather than the worst-measured
   arrangement there is. The drawer holds the long tail. */
.gn-primary {
    display: none;
    min-width: 0;
}

.gn-primary a {
    display: flex;
    align-items: center;
    padding: 0 .8rem;
    font-size: var(--gn-fs-xs);
    letter-spacing: .08em;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--gn-text-muted);
    text-decoration: none;
    white-space: nowrap;
    border-bottom: 2px solid transparent;
}

.gn-primary a:hover {
    color: var(--gn-text);
    background: var(--gn-surface-2);
}

.gn-primary a.active {
    color: var(--gn-text);
    border-bottom-color: var(--gn-accent);
}

.gn-bar-right {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: var(--gn-touch-gap);
    min-width: 0;
}

/* The two rules that used to live here — folding .gn-primary away on a post page,
   and clipping the bars inside .gn-bar-right — went with the reader bars when they
   moved to the strip (owner, 2026-08-08). The links now keep this row on every
   page, which is what ADR-042 §2 wanted in the first place; S1.2's amendment item 2
   was a symptom of the bars being up here, not a decision about the links. */

/* The drawer trigger: filled, ≥44px, and labelled "Menu" in text — never a bare
   icon (ADR-042 §2, the measured recognition position). */
.gn-menu {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--gn-space-2);
    min-height: var(--gn-touch);
    min-width: var(--gn-touch);
    padding: 0 .85rem;
    background: var(--gn-accent);
    color: var(--gn-on-accent);
    border: 0;
    cursor: pointer;
    font-size: var(--gn-fs-xs);
    font-weight: 700;
    letter-spacing: .12em;
    text-transform: uppercase;
    user-select: none;
    margin: 0;
}

.gn-menu:hover {
    background: var(--gn-accent-hover);
}

/* The real control is visually hidden, so its focus ring has to ride the label. */
.gn-nav-toggle:focus-visible ~ .gn-bar .gn-menu {
    outline: 2px solid var(--gn-text);
    outline-offset: 2px;
}

/* ── The drawer: a disclosure, not a dialog (ADR-042 §3) ─────────────────── */
.gn-scrim {
    position: fixed;
    inset: 0;
    z-index: 1050;
    background: #000;
    opacity: 0;
    visibility: hidden;
    transition: opacity .2s ease, visibility .2s ease;
    cursor: pointer;
    margin: 0;
}

.gn-nav-toggle:checked ~ .gn-scrim {
    opacity: .55;
    visibility: visible;
}

.gn-drawer {
    position: fixed;
    z-index: 1100;
    top: 0;
    right: 0;
    width: min(88vw, 360px);
    height: 100dvh;
    display: flex;
    flex-direction: column;
    background: var(--gn-sidebar-bg);
    color: var(--gn-sidebar-text);
    border-left: 2px solid var(--gn-accent);
    /* transform, never `left`: animating `left` thrashes layout every frame.
       visibility:hidden is what takes its links out of the tab order when closed. */
    transform: translateX(100%);
    visibility: hidden;
    transition: transform .22s ease, visibility .22s ease;
}

.gn-nav-toggle:checked ~ .gn-drawer {
    transform: translateX(0);
    visibility: visible;
}

@media (prefers-reduced-motion: reduce) {
    .gn-scrim, .gn-drawer {
        transition: none;
    }
}

.gn-drawer-head {
    flex: none;
    height: var(--gn-bar-h);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 .75rem 0 1rem;
    border-bottom: 1px solid var(--gn-border);
    font-size: var(--gn-fs-xs);
    font-weight: 700;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

.gn-drawer-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--gn-touch);
    min-height: var(--gn-touch);
    cursor: pointer;
    background: var(--gn-surface-2);
    color: var(--gn-text);
    border: 0;
    font-size: var(--gn-fs-lg);
    margin: 0;
}

.gn-drawer-close:hover {
    background: var(--gn-accent);
    color: var(--gn-on-accent);
}

/* The single exception to "zero inner scrollers" (ADR-042 §7) — and only because
   the drawer is chrome. While it is open it is now the ONLY scroller: the root's
   scroll is locked up in §3a, so this is one scrollbar rather than two. */
.gn-drawer-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding-bottom: var(--gn-space-4);
}

.gn-navgroup {
    padding-top: var(--gn-space-3);
}

/* Deliberately NOT a heading (ADR-042 §3): the drawer sits before <h1> in the DOM,
   so a real heading here would open the page outline at h3. role="group" +
   aria-label on the wrapper carries the grouping and owes the outline nothing. */
.gn-navgroup-label {
    margin: 0 0 var(--gn-space-1);
    padding: 0 1rem;
    font-size: var(--gn-fs-xs);
    font-weight: 700;
    line-height: 1.4;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--gn-accent);
}

.gn-navgroup a,
.gn-navgroup button {
    display: flex;
    align-items: center;
    gap: .7rem;
    width: 100%;
    min-height: var(--gn-touch);
    padding: .4rem 1rem;
    color: var(--gn-sidebar-text);
    text-decoration: none;
    font-size: var(--gn-fs-sm);
    text-align: left;
    background: none;
    border: 0;
    border-left: 3px solid transparent;
    cursor: pointer;
}

.gn-navgroup :is(a, button):hover {
    background: var(--gn-surface-2);
    color: var(--gn-text);
    border-left-color: var(--gn-accent);
}

.gn-navgroup a.active {
    border-left-color: var(--gn-accent);
    background: var(--gn-surface-2);
}

.gn-navgroup :is(a, button) .bi {
    color: var(--gn-text-muted);
    width: 1.1em;
    flex: none;
}

.gn-navgroup :is(a, button):hover .bi {
    color: var(--gn-accent);
}

.gn-navgroup form {
    margin: 0;
}

.gn-navgroup-legal a {
    font-size: var(--gn-fs-xs);
    color: var(--gn-text-muted);
}

/* ── The head-start card (game.js .gn-cd) ────────────────────────────────────
   The controls, shown for the five seconds of the countdown and then gone. Lives inside
   #gn-game-overlay, so it inherits pointer-events:none and is removed with the run.

   ABOVE the number, not over it. The number is painted on the canvas at exactly 50%/50%
   at CD_FONT_PX (84px, game.humblecane.js) and pops to 1.18× on each tick, so its top
   reaches 50% − 50px. The offset below is px, not rem, BECAUSE the thing it clears is
   canvas pixels — a rem offset would drift away from it the moment a reader changes their
   type-scale step and the card would land on the number. Change CD_FONT_PX, change this.

   No scrim, unlike #gn-win: the head-start exists so you can watch the level come up, and
   dimming it would defeat the countdown it belongs to. */
.gn-cd {
    position: absolute;
    left: 50%;
    bottom: calc(50% + 62px);
    transform: translateX(-50%);
    max-inline-size: min(92vw, 26rem);
    padding: var(--gn-space-3);
    border: 1px solid var(--gn-border-strong);
    border-radius: var(--gn-radius);
    /* Translucent, so the level keeps showing through the one card that covers it. */
    background: color-mix(in srgb, var(--gn-surface) 86%, transparent);
    box-shadow: var(--gn-shadow);
    text-align: center;
    opacity: 0;
    transition: opacity .28s ease-out;
}

.gn-cd[data-on="1"] {
    opacity: 1;
}

/* ParPar has no countdown number to clear — it has a floor, and the empty band under it is the
   one place on a climb where nothing is happening. game.js writes --gn-cd-y off ParPar's own
   camera each frame, so the card holds its place under the floor rather than drifting when the
   view moves. */
.gn-cd[data-place="floor"] {
    top: var(--gn-cd-y);
    bottom: auto;
}

.gn-cd-head {
    margin: 0 0 var(--gn-space-2);
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-xs);
    text-transform: uppercase;
    letter-spacing: .12em;
}

/* Two columns rather than a run-on line: the keys line up under each other, so the shape of
   the list is readable at a glance in the second or two anyone actually looks at it. */
.gn-cd-list {
    display: grid;
    grid-template-columns: auto auto;
    justify-content: center;
    align-items: center;
    column-gap: var(--gn-space-3);
    row-gap: var(--gn-space-2);
}

.gn-cd-keys {
    display: flex;
    justify-content: flex-end;
    gap: var(--gn-space-1);
}

.gn-cd kbd {
    min-inline-size: 2.2ch;
    padding: .1em .45em;
    border: 1px solid var(--gn-border-strong);
    border-radius: var(--gn-radius-sm);
    background: var(--gn-surface-2);
    color: var(--gn-text);
    font-family: var(--bs-font-monospace);
    font-size: var(--gn-fs-md);
    line-height: 1.4;
}

.gn-cd-act {
    text-align: start;
    font-size: var(--gn-fs-lg);
    font-weight: 600;
    line-height: var(--gn-lh-tight);
    color: var(--gn-text);
}

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

/* ── The cleared-the-run screen (game.js #gn-win) ─────────────────────────────
   Raised by onVictory at three stars, which is the end of the whole progression.
   The one overlay on the site besides the nav drawer that is allowed to be a real
   dialog: the page behind it is already inert while a game runs, it is the only
   thing you can act on, and its single button and Escape do the same thing.
   Removed on deactivate, so it never outlives the run that raised it. */
#gn-win {
    position: fixed;
    z-index: 1150;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: min(92vw, 30rem);
    padding: var(--gn-space-4) var(--gn-space-3);
    text-align: center;
    background: var(--gn-surface);
    border: 2px solid #ffc83d;
    box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .62);
}

.gn-win-stars {
    display: flex;
    justify-content: center;
    gap: var(--gn-space-2);
    font-size: var(--gn-fs-4xl);
    line-height: 1;
    margin-bottom: var(--gn-space-3);
}

/* Each star lands on its own beat (game.js staggers the delay). One shot, not a
   loop: it marks the moment and then stops. */
.gn-win-stars .gn-star {
    animation: gn-win-star .42s ease-out both;
}

@keyframes gn-win-star {
    from { transform: scale(0) rotate(-40deg); opacity: 0; }
    to   { transform: scale(1) rotate(0); opacity: 1; }
}

.gn-win-title {
    font-size: var(--gn-fs-xl);
    font-weight: 700;
    line-height: var(--gn-lh-tight);
    margin: 0 0 var(--gn-space-2);
    color: var(--gn-text);
}

.gn-win-sub {
    font-size: var(--gn-fs-sm);
    color: var(--gn-text-muted);
    margin: 0 0 var(--gn-space-3);
}

.gn-win-stat {
    font-size: var(--gn-fs-lg);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: #ffc83d;
    margin: 0 0 var(--gn-space-3);
}

.gn-win-exit {
    min-height: var(--gn-touch);
    padding: 0 var(--gn-space-4);
    font: inherit;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    background: var(--gn-accent);
    color: var(--gn-on-accent);
    border: 0;
    border-radius: var(--gn-radius-sm);
    cursor: pointer;
}

.gn-win-exit:hover {
    background: var(--gn-accent-hover);
}

.gn-win-hint {
    font-size: var(--gn-fs-xs);
    color: var(--gn-text-muted);
    margin: var(--gn-space-2) 0 0;
}

@media (prefers-reduced-motion: reduce) {
    .gn-win-stars .gn-star {
        animation: none;
    }
}

/* ── Main column and the in-flow legal line ──────────────────────────────── */
/* Centring and the width cap only. The GUTTER sits on the children, not here, and
   that is load-bearing rather than tidy: Bootstrap's `.row` carries a negative
   inline margin that expects a padded parent to bite into. Put the gutter on the
   wrapper and every `.row` inside `.content` escapes it by half a gutter each side
   — 3px of horizontal page scroll on the home page, which is always a bug. */
.gn-wrap {
    max-width: 1360px;
    margin: 0 auto;
}

.content {
    padding-inline: var(--gn-gut);
}

/* ADR-016 §6 wants the legal documents inspectable from every page. The drawer's
   Legal group is two taps away; this is the visible one. In flow, never fixed. */
.gn-legal {
    border-top: 1px solid var(--gn-border);
    margin-top: var(--gn-space-4);
    padding: var(--gn-space-3) var(--gn-gut);
    display: flex;
    flex-wrap: wrap;
    gap: var(--gn-space-2);
    font-size: var(--gn-fs-xs);
    color: var(--gn-text-muted);
}

.gn-legal a {
    color: inherit;
}

/* The bug-report trigger is a <button> because it opens a [popover], not a page — and it has
   to look like the three links beside it, so the button chrome comes off. */
.gn-legal-bug {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
}

.gn-legal-bug:focus-visible {
    outline: 2px solid var(--gn-focus-ring);
    outline-offset: 2px;
}

/* A centred sheet, no z-index: a shown [popover] is in the browser's TOP LAYER, above every
   stacking context on the ladder above. The UA stylesheet already gives it position:fixed,
   inset:0 and margin:auto — the centring is free, and only the surface is ours. Light
   dismiss and Esc come with popover="auto"; there is no JS behind any of this. */
.gn-modal {
    inline-size: min(92vw, 32rem);
    max-block-size: 80dvh;
    overflow: auto;
    padding: var(--gn-space-3);
    border: 1px solid var(--gn-border-strong);
    border-radius: var(--gn-radius);
    background: var(--gn-surface);
    color: var(--gn-text);
}

.gn-modal::backdrop {
    background: rgb(0 0 0 / .55);
}

.gn-modal > :last-child {
    margin-block-end: 0;
}

/* A success notice that removes itself. Row S4.7 retired the Bootstrap toast container at
   1090 and nothing here brings it back: this is one element, no container, no script, and it
   sits at 1150 with the win screen rather than at the retired layer. It clears the strip the
   same way .gn-dafuq-panel does, so the readout is never covered.

   THE FADE IS THE WHOLE MECHANISM. animation-fill-mode: forwards holds the last keyframe, so
   after the run the element is transparent, hidden and click-through for as long as the page
   lives. A timer would need JS; this needs nothing. */
.gn-toast {
    position: fixed;
    z-index: 1150;
    inset: auto var(--gn-gut) calc(var(--gn-strip-h) + var(--gn-gut) + env(safe-area-inset-bottom)) auto;
    max-inline-size: min(90vw, 26rem);
    display: flex;
    align-items: center;
    gap: var(--gn-space-2);
    padding: var(--gn-space-2) var(--gn-space-3);
    border: 1px solid var(--gn-border-strong);
    border-left: 3px solid var(--gn-accent);
    border-radius: var(--gn-radius);
    background: var(--gn-surface);
    color: var(--gn-text);
    font-size: var(--gn-fs-sm);
    /* NOT animated to none at the end, set here for the whole life of the element. Measured in
       the pane: after the fade, `visibility: hidden` in the keyframe does not take on the
       computed value, so the faded toast stayed hittable and ate every click in that corner —
       an invisible dead zone over the page. Nothing in a toast is interactive, so the base rule
       is the fix and the keyframe only has to carry opacity. */
    pointer-events: none;
    animation: gn-toast-in .18s ease-out, gn-toast-out .5s ease-in 5s forwards;
}

.gn-toast .bi {
    color: var(--gn-accent);
}

@keyframes gn-toast-in {
    from { opacity: 0; transform: translateY(.5rem); }
}

@keyframes gn-toast-out {
    to { opacity: 0; }
}

/* Reduced motion loses the slide, not the disappearance — the fade is what makes this a notice
   rather than a permanent fixture, and holding it on screen forever is the worse outcome. */
@media (prefers-reduced-motion: reduce) {
    .gn-toast {
        animation: gn-toast-out .5s ease-in 5s forwards;
    }
}

/* ── The strip: one element, two states (ADR-042 §4) ─────────────────────── */
.gn-strip {
    position: fixed;
    z-index: 1000;
    inset: auto 0 0 0;
    height: calc(var(--gn-strip-h) + env(safe-area-inset-bottom));
    padding-bottom: env(safe-area-inset-bottom);
    background: var(--gn-surface);
    border-top: 2px solid var(--gn-accent);
    display: flex;
    align-items: stretch;
}

.gn-strip-inner {
    width: 100%;
    max-width: 1360px;
    margin: 0 auto;
    padding: 0 var(--gn-gut);
    display: flex;
    align-items: stretch;
    min-width: 0;
}

.gn-strip-idle {
    display: flex;
    align-items: stretch;
    width: 100%;
    min-width: 0;
}

/* ── The strip, PLAYING (ADR-042 §4) ─────────────────────────────────────────
   The same element, the other state. game.js builds the cells; every style for
   them is here, because the strip is chrome and chrome is authored in §3a. The
   deleted #gn-game-hud injected its own stylesheet from JS, which is the whole
   reason its z-index, its gradient and its click-through scaffolding were
   invisible to anyone reading the frame.

   THREE THINGS THE OLD HUD NEEDED AND THIS DOES NOT, all of them consequences of
   floating over the canvas rather than being chrome beside it: a downward
   gradient to read as an overlay (§4 rule 4 — opaque, --gn-surface, 2px accent
   border, which .gn-strip above already carries), a text-shadow under every
   glyph, and pointer-events:none with two opt-ins punched back through it. The
   strip is z-1000 over a z-900 canvas and takes its own clicks.

   ARIA-HIDDEN IS NOT TOGGLED FROM JS, and §4 rule 1 is still satisfied. The two
   states are sibling elements rather than one element rewritten, so whichever is
   idle is display:none and out of the accessibility tree on its own; the playing
   half carries a static aria-hidden="true" (StatusStrip.razor) and its two
   controls carry tabindex="-1" (game.js). What the rule actually forbids — a
   focusable control inside a hidden-but-rendered subtree — cannot occur, and
   there is no toggle to fall out of step with the class. */

/* Taller while playing, and THIS IS THE REFLOW §4 RULE 2 IS ABOUT: adding the
   class resizes the document, and HumbleCane reads scrollHeight exactly once,
   after it. A token value rather than a measurement, so the reflow is settled
   the moment the class lands.

   On :root, not on body, so html's scroll-padding-bottom follows it too — set on
   body and body's own padding updates while the scroll padding stays a state
   behind.

   THE STRIP IS ONE HEIGHT (owner, 2026-08-08). There is no --gn-strip-h override
   for the playing state and there must not be one: the footer is the same size
   whether or not a game is running. The playing readout is the same row of cells
   as the idle one, so it costs the same height by construction — a key line over
   a value, exactly like the handle cell.

   Everything the readout puts in that row has to fit 56px, which is what sizes
   the chip type, the meta cell's two columns and the nudge slider below. The one
   piece that could not was the meta cell at widths where the chips wrap to two
   rows, and the answer is that the meta cell does not appear at those widths —
   see .gn-strip-play .gn-hud-meta.

   This also retires most of ADR-042 §4 rule 2. The strip no longer changes size
   when a game starts, so it no longer moves the document. The ORDERING still
   matters, because body.gn-playing also hides the comment region (ADR-010) and
   that does change the document height — game.js sets the class before extract
   for that reason, not this one. */

.gn-strip-play {
    display: none;
}

body.gn-playing .gn-strip-idle {
    display: none;
}

/* THE SAME ROW OF CELLS AS THE IDLE STATE (owner, 2026-08-08 — this replaces §4
   rule 5's centred three-column grid). Idle is handle · score · trophies ·
   persona · next up; playing is lives · time · score · power-ups · meta. Same
   `.gn-cell` key-over-value shape, same borders, same height — which is what
   lets --gn-strip-h stay one number for both states. */
body.gn-playing .gn-strip-play {
    display: flex;
    align-items: stretch;
    width: 100%;
    min-width: 0;
}

/* FIXED WIDTHS, same lesson as .gn-cell-score on the idle strip: every one of
   these values changes while you are reading it. A timer sized to content
   re-measures ten times a second and drags the whole row with it. */
.gn-cell-lives {
    width: 3.5rem;
    flex: none;
}

.gn-cell-time {
    width: 3.5rem;
    flex: none;
}

.gn-cell-gscore {
    width: 4rem;
    flex: none;
}

.gn-cell-stars {
    width: 3.25rem;
    flex: none;
}

/* Below md the stars lose their label and keep only the glyphs. Every pixel the
   five fixed cells take is one the chip row wraps onto another line with, and of
   the six keys "Round" is the one the row underneath it explains by itself. */
.gn-strip-play .gn-cell-stars .gn-cell-k {
    display: none;
}

@media (min-width: 768px) {
    .gn-strip-play .gn-cell-stars .gn-cell-k {
        display: block;
    }
}

/* The New Game+ stars. Three states, and the middle one is the point: `pending` is the tier you
   are playing for right now, so it breathes while `empty` just sits there. Gold is its own hex
   rather than a token — an earned star means the same thing under all twelve palettes, the same
   argument as --gn-word-* and --gn-c3-* (css-tokens.md). */
.gn-strip-play .gn-stars {
    display: flex;
    align-items: center;
    gap: 2px;
    overflow: visible;
    font-style: normal;
    line-height: 1;
}

.gn-star {
    font-style: normal;
    color: #4a4f57;
}

.gn-star-earned {
    color: #ffc83d;
}

.gn-star-pending {
    color: #b08a1e;
    animation: gn-star-breathe 1.4s ease-in-out infinite;
}

@keyframes gn-star-breathe {
    0%, 100% { opacity: .45; }
    50%      { opacity: 1; }
}

/* Accessibility basic, not polish — a persistent pulse is exactly the motion that triggers
   vestibular symptoms. Keep the distinction, drop the movement. */
@media (prefers-reduced-motion: reduce) {
    .gn-star-pending {
        animation: none;
        opacity: 1;
    }
}

/* Roomier once there is room. The narrow set above is not a preference — under
   768px these three plus the chip row share ~355px inside a 56px strip, and every
   pixel they take is one the chips wrap onto a second line with. */
@media (min-width: 768px) {
    .gn-cell-lives {
        width: 5.5rem;
    }

    .gn-cell-time {
        width: 5rem;
    }

    .gn-cell-gscore {
        width: 8rem;
    }

    /* Wide enough for the word. At 4.5rem "ROUND" clipped to "RO…" — the key is
       uppercase with .16em tracking, which is wider than it looks. */
    .gn-cell-stars {
        width: 5.75rem;
    }
}

/* EVERY RULE BELOW THAT TOUCHES A CELL IS SCOPED TO `.gn-strip-play`, and that is
   load-bearing rather than tidy. These elements carry `.gn-cell` / `.gn-cell-v`
   as well, and those shared rules are defined LATER in this file — at equal
   specificity the later one wins, so an unscoped `.gn-hud-meta { display: none }`
   lost to `.gn-cell { display: flex }` and the meta cell showed at every width.
   The descendant selector makes it (0,2,0) and settles it on specificity instead
   of on where in the file somebody happens to add the next rule. */

/* Ships left, drawn as the ship itself. Red only on the last one — a colour that
   means "danger" from the first frame stops meaning anything by the third.

   The value is a CONTAINER, not a text node: an <svg> is replacing the ▲ glyphs.
   Anything dropped in here is sized to the line box and inherits the colour, so
   `fill: currentColor` on the SVG is all it needs to follow gn-low. It also has
   to undo `.gn-cell-v`'s nowrap/ellipsis clipping, which would cut an SVG off. */
.gn-strip-play .gn-lives {
    display: flex;
    align-items: center;
    gap: 2px;
    letter-spacing: .1em;
    overflow: visible;
}

.gn-strip-play .gn-lives > svg {
    height: 1em;
    width: auto;
    display: block;
    fill: currentColor;
}

.gn-strip-play .gn-lives.gn-low {
    color: var(--gn-danger);
}

/* The round banner rides the score cell's key line. Same size and register as a
   key, accent-coloured because it is an event rather than a label. */
.gn-strip-play .gn-cell-gscore .gn-cell-k {
    color: var(--gn-accent);
}

/* One chip per power-up in its own word colour, wrapping inside its cell. Same
   override as .gn-lives: `.gn-cell-v` clips with an ellipsis, which would eat
   the chips. */
.gn-strip-play .gn-hud-power {
    display: flex;
    align-items: center;
    gap: var(--gn-space-1);
    flex-wrap: wrap;
    white-space: normal;
    overflow: visible;
    font-size: var(--gn-fs-xs);
    font-weight: 700;
    letter-spacing: .04em;
}

/* §4 rule 3: the chip's colour is a --gn-word-* token, painted inline by game.js
   off the same values the word glow reads, so a green chip and a green word are
   the same fact. Those tokens are locked (ADR-044 §3) and nothing here re-tones
   them — the chip is currentColor on the strip's own surface, with no plate of
   its own. The old translucent black plate existed to survive the canvas showing
   through; opaque chrome does not need one.

   The pill radius is the one place --gn-radius is deliberately ignored. §4
   relocates these verbatim and redesigns none of them; a chip is a chip.

   BELOW md THE CHIP IS ITS COLOUR AND ITS COUNT, and nothing else. Five labelled
   chips need ~370px; down there the whole strip has ~355px and three other cells
   in it. Dropping the word keeps all five on one row inside 56px, and ADR-019 §3
   says the colour is the lesson — a green chip beside a green word — so the word
   is the half that can go. game.js writes the count to data-n for exactly this;
   `font-size: 0` hides the label text without hiding the box. */
.gn-chip {
    padding: 0 .35rem;
    border: 1px solid currentColor;
    border-radius: 999px;
    white-space: nowrap;
    font-size: 0;
}

.gn-chip::before {
    content: attr(data-n);
    font-size: var(--gn-fs-xs);
}

@media (min-width: 768px) {
    .gn-chip {
        padding: 0 var(--gn-space-2);
        font-size: var(--gn-fs-xs);
    }

    .gn-chip::before {
        content: none;
    }
}

/* Held at zero: same hue, just quieter — the colour still teaches, and the ones
   you actually hold still pop. */
.gn-chip.gn-off {
    opacity: .42;
}

/* The meta cell: the small print, deliberately quiet, holding the right end of
   the strip. Two columns inside it — read-only print, then the two controls —
   because stacked in one column it is four lines, which is taller than the
   strip. Hidden below xl — see the --gn-strip-h note above: below that width its
   275px is what forces the chip row to wrap, and the chips outrank it. */
.gn-strip-play .gn-hud-meta {
    display: none;
    margin-left: auto;
    flex-direction: row;
    align-items: center;
    gap: var(--gn-space-3);
    text-align: right;
    line-height: var(--gn-lh-tight);
    font-size: var(--gn-fs-xs);
    color: var(--gn-text-muted);
}

@media (min-width: 1200px) {
    .gn-strip-play .gn-hud-meta {
        display: flex;
    }
}

.gn-meta-text {
    min-width: 0;
}

.gn-meta-ctl {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    flex: none;
}

/* …and gone again in a short viewport, which can be wider than xl and still have
   no vertical room for it. Below the xl rule so it wins. */
@media (max-height: 560px) {
    .gn-strip-play .gn-hud-meta {
        display: none;
    }
}

.gn-hint,
.gn-specs {
    font-size: var(--gn-fs-xs);
    color: var(--gn-text-muted);
}

.gn-specs {
    max-width: 22rem;
}

/* The turn-feel slider. `.gn-turn`, NOT `.gn-nudge` — that name is the trophy
   button's pulse further down this file, and the two are now siblings inside
   .gn-strip. */
.gn-turn {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--gn-space-1);
    font-size: var(--gn-fs-xs);
    color: var(--gn-text-muted);
}

.gn-turn input {
    width: 130px;
    cursor: pointer;
}

.gn-turn-val {
    min-width: 3.2em;
    text-align: left;
    font-variant-numeric: tabular-nums;
}

/* A control, so --gn-border-strong: the quiet border does not clear WCAG 1.4.11's
   3:1 on a boundary you are meant to be able to find. */
.gn-adv {
    cursor: pointer;
    font: inherit;
    font-size: var(--gn-fs-xs);
    color: var(--gn-text);
    background: var(--gn-surface-2);
    border: 1px solid var(--gn-border-strong);
    border-radius: var(--gn-radius-sm);
    padding: 0 var(--gn-space-2);
}

.gn-cell {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 1px;
    padding: 0 clamp(.55rem, 2vw, 1.1rem);
    height: 100%;
    border-left: 1px solid var(--gn-border);
    min-width: 0;
    background: none;
    border-top: 0;
    border-right: 0;
    border-bottom: 0;
    text-align: left;
    color: inherit;
}

/* The trophy cell is an <a> since row S4.7 (it navigates to /account/trophies now, rather than
   opening a modal), and every other cell is a <div> or a <button>. Reset the anchor to match:
   `color: inherit` above already handles the hue, this kills the underline. */
a.gn-cell {
    text-decoration: none;
}

.gn-cell:first-child {
    border-left: 0;
    padding-left: 0;
}

/* THE ONE CELL THAT MUST NOT SIZE TO ITS CONTENT. It rotates, and both its lines
   change with it — "Score" (45px) to "Humblebrag Hurricane" (182px) to "This
   article — high score" (205px), the value gaining and losing a rank. Sized to
   content it re-measured on every tick and shoved every cell to its right sideways
   with it (owner, 2026-08-08).

   Fixed, not sized to the longest label: 205px plus the cell's own padding is 240px
   of a strip that also has to hold the reader bars, and the rotation is a background
   readout, not the page. `.gn-cell-k` already clips with an ellipsis, so a long
   board name truncates — consistently, in the same place, without moving anything. */
.gn-cell-score {
    width: 7.5rem;
    flex: none;
}

@media (min-width: 768px) {
    .gn-cell-score {
        width: 11rem;
    }
}

/* A cell that is also the trophy-case trigger. Keeps the 44px target without
   growing the strip: the cell is already full height. */
button.gn-cell {
    cursor: pointer;
    min-width: var(--gn-touch);
    font: inherit;
}

button.gn-cell:hover {
    background: var(--gn-surface-2);
}

.gn-cell-k {
    font-size: var(--gn-fs-xs);
    line-height: 1;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
    white-space: nowrap;
    /* At 375px a cell is ~77px wide and "TROPHIES" overruns it — clip rather than
       let it spill across the neighbour's border. */
    overflow: hidden;
    text-overflow: ellipsis;
}

.gn-cell-v {
    font-size: var(--gn-fs-sm);
    line-height: 1.15;
    font-weight: 700;
    color: var(--gn-text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-variant-numeric: tabular-nums;
}

.gn-cell-v .gn-sig {
    color: var(--gn-accent);
}

.gn-cell-hint {
    font-size: var(--gn-fs-xs);
    line-height: 1.1;
    color: var(--gn-text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 30ch;
}

.gn-cell-hide {
    display: none;   /* restored at md */
}

/* Endowed progress, not a bare 6/24. */
.gn-meter {
    display: flex;
    gap: 2px;
    margin-top: 2px;
}

.gn-meter i {
    width: 4px;
    height: 6px;
    background: var(--gn-border);
}

.gn-meter i.on {
    background: var(--gn-accent);
}

/* ADR-042 §4's reserved region. It is no longer a stub: the owner folded the
   ADR-013 §7 reader bars into it (2026-08-08), down from the top bar where they
   cost the primary links their row. margin-left:auto is still the whole of its
   layout contract — it holds the right end of the strip. */
.gn-strip-nav {
    margin-left: auto;
    display: flex;
    align-items: center;
    min-width: 0;
}

/* The bars are a form of two labelled sliders, two checkboxes and Apply. It stays
   on one line and gives up width before it gives up height — the strip's height is
   a token, and the game readout's reflow depends on it not moving.

   OPEN: THE NARROW PLACEMENT IS NOT DECIDED. The form needs ~546px and gets it from
   lg up, comfortably. Below that its children overflow their own box, so `overflow:
   hidden` clips the tail rather than letting it overlap the Handle/Score/Trophies
   cells. That is exactly the treatment `.gn-bar-right` gave it before the move — no
   regression, but no answer either. The real options are a second strip row on
   narrow screens, dropping the two text labels below lg, or moving the bars back
   into the post body below lg. Owner's call, not this row's. */
.gn-strip-nav .gn-reader-bars {
    flex-wrap: nowrap;
    min-width: 0;
    overflow: hidden;
}

/* Row S4.2: the bars were still on Bootstrap's .form-label / .form-check-label defaults
   from before the strip existed, so "Vulgarity", "Jargon" and "Auto" sat at body size
   beside cells whose keys are .16em micro-caps. They are the same kind of label as the
   HANDLE / SCORE / TROPHIES keys two cells to their left and now read as one row.
   Sized down, which also buys back width in the one place that has none — see the OPEN
   note above; this narrows the overflow, it does not settle where the bars go. */
.gn-strip-nav .gn-reader-bars .form-label,
.gn-strip-nav .gn-reader-bars .form-check-label {
    font-size: var(--gn-fs-2xs);
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

/* Five positions, so the track only has to be long enough to hit five stops — the
   default width was sized for a slider that sweeps, not one that notches (owner,
   2026-08-08). At 5rem the thumb travels 4rem, which is 1rem a stop. The notch
   (--gn-mark) is a percentage of the track, so it follows this on its own.

   ponytail: a range input for a five-value choice is the wrong control and this is
   compression, not a fix — the owner is changing how these work later. */
.gn-strip-nav .form-range {
    max-width: 5rem;
}

/* Apply is a real control on a strip a thumb can reach, so it takes the touch
   floor rather than btn-sm's ~31px. The Auto checkboxes do NOT — growing those to
   44px each would push the whole form off the strip, and they are pre-existing.
   Worth a look on a real device before it is called done. */
.gn-strip-nav .btn {
    min-height: var(--gn-touch);
}

/* ── The game toggles, in the strip (owner, 2026-08-11) ─────────────────────
   Moved out of the post's top-right (ADR-013's 2026-07-21 amendment) to sit beside the reader
   bars. GameButtons.razor emits them on every page — every gate it can check is a claim — and
   THIS IS THE GATE IT CANNOT: a playable article on screen. `.blog-post` is the games' own
   applicability rule (ADR-042 §6), so reading it here means the button and the engine can never
   disagree about whether there is anything to play. game.js also disables them off a post; the
   two agree, and this one is what stops a pair of dead buttons riding the chrome site-wide. */
body:not(:has(.blog-post)) .gn-strip-games {
    display: none;
}

.gn-strip-games {
    display: flex;
    align-items: center;
    gap: var(--gn-space-1);
    margin-inline-start: var(--gn-space-2);
}

/* 60px was sized for a post's header; the strip is 56px tall and the glow needs room to breathe
   inside it. Overrides game.js's injected block on specificity (0,2,0 beats 0,1,0), which is what
   lets the animation and the disabled state stay where they are — only the geometry moves here.
   The glow itself is untouched: it is the one thing on the strip allowed to pulse. */
.gn-strip-games .gn-game-btn {
    width: 34px;
    height: 34px;
    border-radius: 10px;
    font-size: var(--gn-fs-md);   /* was a literal 1.05rem — the scale step is ~5% smaller
                                     in a 34px box, invisible, and now follows the dial */
}

/* The glow is a box-shadow spreading up to 6px past the button, and neither .gn-strip nor
   .gn-strip-inner sets overflow — so it spills over the strip's top edge instead of being
   clipped into a rectangle. Checked, not assumed: adding `overflow: hidden` anywhere up that
   chain would silently square off the pulse. */

.gn-strip-signin {
    display: inline-flex;
    align-items: center;
    gap: var(--gn-space-2);
    align-self: center;
    margin-left: auto;
    min-height: var(--gn-touch);
    padding: 0 .85rem;
    background: var(--gn-accent);
    color: var(--gn-on-accent);
    text-decoration: none;
    font-size: var(--gn-fs-xs);
    font-weight: 700;
    letter-spacing: .12em;
    text-transform: uppercase;
}

.gn-strip-signin:hover {
    background: var(--gn-accent-hover);
    color: var(--gn-on-accent);
}

@media (min-width: 768px) {
    .gn-brand small {
        display: inline;
    }

    .gn-cell-hide {
        display: flex;
    }
}

@media (min-width: 992px) {
    .gn-primary {
        display: flex;
    }
}

/* ── 3. gn component classes (shared — must NOT live in scoped .razor.css) ── */
.gn-hero {
    padding: var(--gn-space-5) var(--gn-space-3) var(--gn-space-4);
}

/* Row S4.5: was `.lead gn-tagline` in the markup, and .lead was a bare 1.25rem literal. The
   size belongs on the tagline itself now — one step up from body, off the scale, so the
   reader's type control moves it like everything else. */
.gn-tagline {
    color: var(--gn-text-muted);
    max-width: var(--gn-measure);
    margin: 0 auto;
    font-size: var(--gn-fs-lg);
}

.gn-card {
    background: var(--gn-surface);
    border: 1px solid var(--gn-border);
    border-radius: var(--gn-radius);
    box-shadow: var(--gn-shadow);
}

.gn-card .card-title {
    color: var(--gn-text);
}

.gn-badge {
    background: var(--gn-border);
    color: var(--gn-text-muted);
}

/* The C3 tuner's read-only colour chip, sized to match Bootstrap's .form-control-color so a
   following value and an overridden one occupy the same box and the two columns stay level. It
   exists because an <input type="color"> cannot HOLD a token — it coerces anything it does not
   understand to #000000 — but that is a reason to hide the INPUT, not the colour.
   --gn-border-strong because it reads as a control even though it is not one (WCAG 1.4.11).

   `.gn-swatch` WAS ALREADY TAKEN by Appearance.razor's palette picker (a 22px chip, further down
   this file), which silently won on source order and rendered this one at 22px. Third time this
   class of clash has bitten — see ADR-042's `.gn-bar` and `.gn-nudge`. Grep before you name. */
.gn-c3-swatch {
    display: block;
    width: 3rem;
    height: calc(1.5em + .75rem + 2px);
    border: 1px solid var(--gn-border-strong);
    border-radius: var(--gn-radius);
}

/* Rendered blog-post body (Markdig output injected as raw HTML, so scoped
   .razor.css can't reach it — shared class per the styling convention). */
.blog-post img,
.blog-post iframe,
.blog-post video {
    max-width: 100%;
    border-radius: var(--gn-radius);
}

.blog-post img {
    height: auto;
}

/* ── Breakout content grid — ADR-045 §3 (row S3.1) ──────────────────────────
   A named-line grid that lets one block escape the reading measure without leaving the
   flow. Children sit in `main` by default; `.gn-wide` and `.gn-full` opt out.

   THIS REPLACES THE max-width CAP, it does not sit beside it. `min(var(--gn-measure),
   100%)` — S1.3's amendment to ADR-042 §7 — is now the `main` track, and the `100%` half
   is still the thing that stops horizontal page scroll, so it must survive any edit here.

   THE MEASURE IS IN `ch`, NEVER px (ADR-042 §7), and it is a game change as much as a
   reading one: a pixel width silently becomes a 95-character line the moment somebody
   picks a narrower typeface, and every font in ADR-044's list has a different one. But a
   68ch column also makes an article TALLER while HumbleCane's word cap stays at 700 and
   its pan speed stays put — an emptier, slower level. The dial for that is the cap or the
   speed, not the layout, and which one moves is a playtest answer (S1.3).

   THE GRID GOES ON .blog-post, NEVER ON .content (ADR-045 §3). --gn-gut lives on
   .content precisely so Bootstrap's .row has a padded parent to bite into. Make .content
   a grid and every .row on every non-article page becomes a grid item, its negative
   inline margin resolves against the wrong box, and rows escape the gutter — horizontal
   page scroll on pages that have nothing to do with this component. Prose outside an
   article opts in with .gn-prose instead.

   The article body is injected as a MarkupString, so Markdig's output blocks are direct
   children of .blog-post and become the grid items. That is the mechanism, not a side
   effect: raw HTML passthrough is on by design, so an author writes
   <figure class="gn-full"> for a full-bleed image.

   COST, PAID KNOWINGLY: a grid item establishes its own formatting context, so a float
   inside one no longer wraps the blocks after it. `![](url){.float-start .me-3}` (the
   Editor still documents it) now floats within its own paragraph only. No live post uses
   it. That is also why .blog-post::after — a clearfix for exactly those floats — was
   deleted rather than ported.

   `clip`, not `hidden`: both bound the article, but `hidden` makes a scroll container,
   and a scroll container between the viewport and a `position: sticky` descendant is how
   §6's illustration pane silently stops sticking. `clip` creates no scrollport. */
.blog-post,
.gn-prose {
    display: grid;
    grid-template-columns:
        [full-start] minmax(var(--gn-gut), 1fr)
        [wide-start] minmax(0, var(--gn-space-5))
        [main-start] min(var(--gn-measure), 100%) [main-end]
                     minmax(0, var(--gn-space-5)) [wide-end]
                     minmax(var(--gn-gut), 1fr) [full-end];
    overflow-x: clip;
    overflow-wrap: anywhere;
}

/* min-width: 0 is not defensive tidying — a grid item's `auto` minimum is its content, so
   one unbreakable string in a paragraph would widen the `main` track past the measure. */
.blog-post > *,
.gn-prose > * {
    grid-column: main;
    min-width: 0;
}

.blog-post > .gn-wide,
.gn-prose > .gn-wide {
    grid-column: wide;
}

.blog-post > .gn-full,
.gn-prose > .gn-full {
    grid-column: full;
}

.blog-post pre {
    max-width: 100%;
    overflow-x: auto;
}

.blog-post table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
}

/* An author's TAB, rendered as the editor renders it: about one character wide (AuthorSpacing.cs
   emits these). 1ch = the width of "0" in the post's own font, so it tracks persona themes and
   font-size changes instead of being a fixed pixel guess. THE calibration knob for
   editor-vs-post drift — measured at 8.63px against the editor's ~8.8px per tab. */
.blog-post .gn-tab {
    display: inline-block;
    width: 1ch;
}

/* Verbatim layout block. Markdown reflows prose and strips leading indentation by design;
   wrap a section in <pre class="gn-layout">…</pre> to keep your spaces, tabs, newlines and
   blank lines exactly as typed (Markdig passes a raw <pre> block through untouched — must be
   <pre>, since a <div> block would end at the first blank line). pre-wrap overrides <pre>'s
   no-wrap so the page stays bound to its width — used to spread words across the play area.
   Reset the code-ish chrome a bare <pre> would otherwise inherit; monospace stays for aligned
   columns. */
.blog-post pre.gn-layout {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    color: inherit;
}

/* ── The article page — ADR-042 §7 (row S4.2) ───────────────────────────────
   THE MEASURE WAS NOT THIS ROW'S PROBLEM. S3.1 made `main` the `min(--gn-measure,
   100%)` track above and S1.3 signed it off; nothing here touches it. What this row
   found is that the measure was the ONLY thing on the page obeying it — the header, the
   colophon and the whole comment region were full-width children of `.content` while the
   prose sat in a centred 68ch column, so every boundary stepped ~280px left and the page
   read as two documents that had missed each other.

   `.gn-measured` is the whole fix, and it is deliberately NOT a grid. `.content` must
   never become one (see the breakout-grid note above: Bootstrap's `.row` needs a padded
   BLOCK parent, and every non-article page would lose its gutter). A max-width plus auto
   margins lands on exactly the same left edge, because the grid's two gutter tracks are
   symmetric `1fr`s and therefore centre `main`. Verified against the computed value, not
   assumed — `.blog-post` resolves to a 637.5px `main` at 1201px, and so does this. */
.gn-measured {
    max-width: min(var(--gn-measure), 100%);
    margin-inline: auto;
}

/* ── The header: kicker · title · dek · byline ──────────────────────────────
   L3's article head (design-explore/L3-console.html §8), resolved onto the type scale
   rather than copied as literals, the same way S4.1 did the index — every size here is a
   --gn-fs-* step so the reader's type control still moves it (ADR-044 §4).

   EVERYTHING EXCEPT THE TITLE CARRIES data-gn-noscan, in the markup. The extractor roots
   at `.content` and gamifies "title + body" on purpose (game.humblecane.js:370), so the
   title stays a target exactly as it was before this row — but the kicker, dek, byline
   and tags are new text inside that root, and without the mark ~20 words of chrome
   ("TECHNICAL", "min read", "By") would have been lifted into every level. That is
   ADR-042 §6's rule applied to metadata that did not exist when it was written. */
.gn-article-head {
    margin-block-end: var(--gn-space-4);
    padding-block-end: var(--gn-space-3);
    border-block-end: 1px solid var(--gn-border);
}

/* While a game runs, the head keeps its TITLE (a deliberate target — §6's "title + body")
   and loses its metadata: kicker, dek, byline and the hairline all read as floating chrome
   inside a level (owner, 2026-08-13, S7.0 close-out — "date, by line, break line are
   visible on the screen"). Safe against the reflow-before-extract rule for the same reason
   the comment region is: game.js sets body.gn-playing BEFORE extraction, so the level is
   built from the collapsed document. All four carry data-gn-noscan already, so no level
   words are lost — this is the visual half of the same exclusion. */
body.gn-playing .gn-article-kick,
body.gn-playing .gn-article-dek,
body.gn-playing .gn-article-byline {
    display: none;
}

body.gn-playing .gn-article-head {
    border-block-end: 0;
    padding-block-end: 0;
}

/* The micro-label row. Same .16em caps as the index's column heads — it is the same
   voice, and the two pages are seen one after the other. */
.gn-article-kick {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gn-space-2);
    margin-block-end: .6rem;
    font-size: var(--gn-fs-2xs);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

/* The category is the one loud thing in the row — it is the only part a reader uses to
   place the article before reading it. */
.gn-article-kick b {
    color: var(--gn-accent);
    font-weight: 700;
}

/* No font-size here on purpose: the global h1 rule already clamps between two scale
   steps, so this only tightens what a display line needs (css-tokens: never write a
   literal font-size — if no step fits, the scale is wrong). */
.gn-article-title {
    margin: 0 0 var(--gn-space-2);
    line-height: 1.08;
    letter-spacing: -.02em;
}

/* Narrower than the prose it introduces. A standfirst set to the full measure reads as
   a first paragraph, which is the one thing it must not be mistaken for. */
.gn-article-dek {
    margin: 0 0 var(--gn-space-3);
    max-width: 58ch;
    font-size: var(--gn-fs-lg);
    line-height: 1.45;
    color: var(--gn-text-muted);
}

.gn-article-byline {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gn-space-2) var(--gn-space-3);
    font-size: var(--gn-fs-xs);
    color: var(--gn-text-muted);
}

/* L3's `By — Name` pair. A real <dl>, not two spans: it is a term and its value, and that is
   what makes the row read as a byline rather than as a caption. The label keeps the micro-cap
   voice; the name is body ink, because the name is the information. */
.gn-article-byline dl {
    display: flex;
    align-items: baseline;
    gap: .4rem;
    margin: 0;
}

.gn-article-byline dt {
    font-size: var(--gn-fs-2xs);
    letter-spacing: .14em;
    text-transform: uppercase;
}

.gn-article-byline dd {
    margin: 0;
    font-weight: 700;
    color: var(--gn-text);
}

/* ── Prose rhythm ───────────────────────────────────────────────────────────
   Before this row the article body had four rules — img, pre, table and .gn-tab — so
   every paragraph, heading, list and quote fell through to Bootstrap's reboot: a flat
   16px gap between everything, and a heading spaced identically to the paragraph above
   and the paragraph below it.

   THE ONE IDEA HERE: space belongs ABOVE a heading, not below it. A subhead sits with
   the text it introduces; an evenly-spaced one floats between two blocks and belongs to
   neither. Everything else is that principle plus the token scale.

   Scoped to .blog-post and .gn-prose together, exactly as the breakout grid above is —
   they are the two prose surfaces and they must not drift apart. */
.blog-post,
.gn-prose {
    line-height: var(--gn-lh);
}

.blog-post :is(h2, h3, h4, h5, h6),
.gn-prose :is(h2, h3, h4, h5, h6) {
    margin-block: var(--gn-space-4) var(--gn-space-2);
}

/* An article opening on a subhead must not push itself down the page. */
.blog-post > :first-child,
.gn-prose > :first-child {
    margin-block-start: 0;
}

.blog-post p,
.gn-prose p {
    margin-block: 0 var(--gn-space-3);
}

/* L3's console mark on the two subhead levels an author actually writes. Generated
   content, so it is invisible to the word extractor's TreeWalker (it walks text NODES)
   and cannot become a game target — checked, because a "##" target would be absurd. */
.blog-post :is(h2, h3)::before,
.gn-prose :is(h2, h3)::before {
    content: "## ";
    color: var(--gn-accent);
}

.blog-post h3::before,
.gn-prose h3::before {
    content: "### ";
}

/* ::marker rather than L3's `list-style: none` + a ::before glyph. Same accent bullet,
   and it keeps the list a list — Safari drops list semantics from VoiceOver the moment
   list-style goes to none, which would have made this a quiet accessibility regression
   in exchange for nothing. */
.blog-post :is(ul, ol) > li::marker,
.gn-prose :is(ul, ol) > li::marker {
    color: var(--gn-accent);
}

.blog-post li,
.gn-prose li {
    margin-block-end: .45rem;
}

/* L3's pull quote, as the plain <blockquote> an author gets from Markdown for free —
   no class to remember, so it is what they will actually type. */
.blog-post blockquote,
.gn-prose blockquote {
    margin-block: var(--gn-space-4);
    padding: var(--gn-space-3);
    border-inline-start: 3px solid var(--gn-accent);
    background: var(--gn-surface);
}

.blog-post blockquote > :last-child,
.gn-prose blockquote > :last-child {
    margin-block-end: 0;
}

/* Solid and themed, like .gn-section-break — Bootstrap's default hr is an opacity trick
   that all but vanishes on half the twelve palettes. */
.blog-post hr,
.gn-prose hr {
    margin-block: var(--gn-space-4);
    border: 0;
    border-block-start: 1px solid var(--gn-border);
    opacity: 1;
}

.blog-post figure,
.gn-prose figure {
    margin-block: var(--gn-space-4);
}

.blog-post figcaption,
.gn-prose figcaption {
    margin-block-start: var(--gn-space-2);
    font-size: var(--gn-fs-xs);
    color: var(--gn-text-muted);
}

/* ── Sticky illustration pane — ADR-045 §6 (row S3.6) ───────────────────────
   A two-column region where the prose scrolls and an illustration holds position beside
   it. Markup contract:

     <div class="gn-illus">
       <figure class="gn-illus-fig" data-gn-noscan>…</figure>   <-- FIRST in the DOM
       <div class="gn-illus-body">…prose…</div>
     </div>

   THE FIGURE COMES FIRST IN SOURCE and is placed into column 2 at lg. That ordering is
   the whole of the narrow-viewport rule (ADR-045 §6): below lg there is no placement, the
   two blocks stack in source order, and the illustration lands ABOVE the passage it
   illustrates. An illustration that arrives after the passage explaining it is not an
   illustration.

   align-self: start IS REQUIRED and is the single most common way this component silently
   does nothing: a grid item stretches to its row by default, and a full-height sticky
   element has no slack inside its container to move within, so it never unsticks from the
   top and never appears to stick at all.

   `top` clears the fixed bar. Stuck at 0 it would slide underneath it.

   `sticky`, NEVER `fixed` (ADR-042 §7): fixed leaves document coordinate space outright,
   and both games extract words once, in document coordinates. sticky is why the pane's
   text still carries data-gn-noscan (ADR-045 §2 rule 6) — it moves relative to the
   document after extraction, so its words would be shootable where they no longer are.
   The attribute is the author's job; CSS cannot add it.

   Nothing here creates a scroll container, and nothing may: any ancestor that scrolls
   between this and the viewport captures the stickiness. That is why §3 above clips with
   `clip` rather than `hidden` — a pane used inside an article depends on it.

   NO INNER SCROLLER (§2 rule 7). The pane is authored to fit the viewport; one taller
   than the viewport simply stops being sticky rather than growing a scrollbar.

   No :hover / :focus-visible / disabled block (§2 rule 9) and no motion (§2 rule 10): the
   pane is a region, never a control. Whatever it holds owns its own states.

   Column width is in `ch` (§2 rule 8), so it tracks the reader's type step and the twelve
   font choices instead of being a pixel guess. Inside an article, give the whole pane
   `.gn-wide` or `.gn-full` — at `main` there is no room for two columns. */
.gn-illus {
    display: grid;
    gap: var(--gn-space-3);
}

@media (min-width: 992px) {
    .gn-illus {
        grid-template-columns: minmax(0, 1fr) minmax(0, 34ch);
        gap: var(--gn-space-4);
    }

    .gn-illus-body {
        grid-column: 1;
        grid-row: 1;
    }

    .gn-illus-fig {
        grid-column: 2;
        grid-row: 1;
        align-self: start;
        position: sticky;
        top: calc(var(--gn-bar-h) + var(--gn-space-3));
        margin: 0;
    }
}

/* ── Progress ring — ADR-045 §8 (row S3.5) ──────────────────────────────────
   Drawn by ProgressRing.razor. Two concentric strokes on r=45; the arc length is a
   server-computed stroke-dasharray, so there is no script and nothing to measure.

   Sized in em, not on the spacing scale: the ring's job is to sit at the size of the
   text beside it, so it inherits the type step of whatever draws it rather than
   picking one. Geometry stays unitless inside the viewBox (ADR-045 §8) — it is a
   shape, not a spacing decision, so it is deliberately not tokenised.

   No :hover / :focus-visible / disabled block (§2 rule 9): the ring is never itself a
   control. Where it sits inside one — the trophy tile, the strip cell — that control
   owns the interactive states and the ring just rides along. No motion either, so
   nothing for prefers-reduced-motion to switch off.

   CONTRAST, AND THE ONE PLACE THIS FALLS SHORT OF §8. Both strokes clear their own leg
   against the ground in all twelve palettes: --gn-border-strong is held to 3:1 by
   palette-contrast.py, and --gn-accent measures 5.58–14.27:1. What does NOT clear is
   the fill against the track — 1.40:1 (ember-light) to 2.51:1 (terminal-green), where
   §8 asks for 3:1. That is not a tuning miss: a sweep of every token pair in the twelve
   palettes tops out at 2.51:1 (--gn-text on --gn-border-strong), so no existing pairing
   satisfies both legs and the fix is a palette change, not a CSS one. It is survivable
   because WCAG 1.4.11 exempts a boundary whose information is also available in text,
   which is exactly the Label-is-null case the component is built around — the number is
   stated beside the ring. A standalone ring (Label set) is the case that is genuinely
   short, so prefer a visible sibling. Re-run docs/projects/style/palette-contrast.py
   after any palette edit. */
.gn-ring {
    /* THE KNOB. Unitless because it is viewBox user units, not CSS px — it does not scale with
       the type step, it is a proportion of the ring itself, so one value holds at every size.
       Override per instance (a dense list may want it thinner) rather than editing this default:
           .gn-index-c-ai .gn-ring { --gn-ring-stroke: 10 }
       CEILING IS 20, set by ProgressRing.razor's Radius of 40: the stroke grows both ways from
       the centreline, so anything past 2 × (50 − Radius) is clipped by the viewBox. Want thicker?
       Lower Radius — the circumference and every dasharray follow it automatically. */
    --gn-ring-stroke: 13;

    width: 1em;
    height: 1em;
    flex: none;                 /* a meter beside text must not be squeezed by its flex parent */
    vertical-align: -0.15em;    /* optically centred on the x-height rather than sat on the baseline */
}

/* Per-instance size override, the pattern the block above describes. The trophy case's summary
   dial is a graphic rather than a ring sitting in a line of text: no type step reaches 4rem, and
   `1em` would tie it to whatever step the caption happens to use. It lives here and not in
   TrophyCase.razor.css because the <svg> is ProgressRing's markup and carries ProgressRing's
   scope — a scoped rule in the calling component never matches it. */
.gn-case-dial .gn-ring {
    inline-size: var(--gn-space-5);
    block-size: var(--gn-space-5);
    vertical-align: baseline;   /* the -.15em nudge is for a ring optically centred on x-height */
}

.gn-ring-track,
.gn-ring-fill {
    fill: none;
    stroke-width: var(--gn-ring-stroke);
    /* §8: butt, never round. A round cap at 0% draws a visible dot — a ring that reads as
       "started" for a reader who has not started. */
    stroke-linecap: butt;
}

.gn-ring-track {
    stroke: var(--gn-border-strong);
}

.gn-ring-fill {
    stroke: var(--gn-accent);
}

/* ── Article index table — ADR-042 §7 (row S4.1) ────────────────────────────
   /blog's list of articles, ported from design-explore/L3-console.html §02.

   THIS REPLACED A SUBGRID <ul> (S3.2), and the reversal is recorded in ADR-045 §4.
   Short version: a six-column data grid wants column headers, a hover row and zebra,
   and a list can give none of the three — a header inside a <ul> is a fake <li>, and a
   full-bleed row needs the row gap the list is built around. A table announces "Tags,
   column 3: themes" on every cell, which beats "list, 12 items" for this surface. The
   price is ADR-042 §8's card-below-md WITH ROLES RE-ASSERTED, which is written below
   and is the half that stops the card flip being an accessibility regression.

   Sizes are L3's, resolved onto the type scale rather than copied as literals: every
   font-size here is a --gn-fs-* step so the whole index still moves with the reader's
   size preference (ADR-044 §4). The steps land within ~7% of the mockup, and --gn-fs-2xs
   was added to §1 for the micro-labels L3 leans on. Spacing IS L3's, to the 0.05rem —
   padding is not on the type scale and has no reader control to preserve. */

/* L3's section head: label, meta on the right, accent rule under both.
   Shared with Gaslight Alley (row S4.2) rather than copied — it is the same head in the
   same voice, and the index and an article's comment section are seen one after the
   other. The `gn-index-` name stays because that is where it was authored; a second
   class on the same declarations is cheaper than renaming three rules and their call
   sites for tidiness. */
.gn-index-head,
.gn-comments-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--gn-space-2);
    margin-block: var(--gn-space-4) 0;
    padding-block-end: .35rem;
    border-block-end: 2px solid var(--gn-accent);
}

.gn-index-head h2,
.gn-comments-head h2 {
    margin: 0;
    font-size: var(--gn-fs-xs);
    font-weight: 700;
    letter-spacing: .16em;
    text-transform: uppercase;
}

.gn-index-head-meta,
.gn-comments-head-meta {
    font-size: var(--gn-fs-2xs);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

.gn-index {
    width: 100%;
    border-collapse: collapse;
    margin: 0;
    color: var(--gn-text);
}

/* Not display:block (L3 sets that and the caption collapses into the first column's
   width). table-caption spans the table, which is the whole point of a caption. */
.gn-index caption {
    caption-side: top;
    padding: 0 0 .5rem;
    font-size: var(--gn-fs-2xs);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
    text-align: left;
}

/* Card mode has no visible header row — but NOT display:none, which would drop the
   column headers out of the accessibility tree, and the re-asserted role="columnheader"
   is the whole reason the card flip is safe. The visible replacement (td::before,
   generated content) is not an accessible name. Hide it visually, keep it announced.
   Undone in the md block below. */
.gn-index thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

.gn-index th,
.gn-index td {
    text-align: left;
}

/* ── base (< md): card up ─────────────────────────────────────────────────
   display:block + data-label, with the roles re-asserted in the markup. */
.gn-index,
.gn-index tbody,
.gn-index tr,
.gn-index td {
    display: block;
}

.gn-index tbody tr {
    border: 1px solid var(--gn-border);
    border-inline-start: 3px solid var(--gn-border);
    background: var(--gn-surface);
    padding: .7rem .85rem;
    margin-block-end: -1px;    /* collapse the shared edge between stacked cards */
}

.gn-index tbody tr:hover,
.gn-index tbody tr:focus-within {
    border-inline-start-color: var(--gn-accent);
}

.gn-index td {
    display: flex;
    gap: .75rem;
    align-items: baseline;
    padding: .18rem 0;
    font-size: var(--gn-fs-sm);
}

.gn-index td::before {
    content: attr(data-label);
    flex: none;
    width: 5.5em;
    font-size: var(--gn-fs-2xs);
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
    line-height: 1.9;
}

.gn-index td.gn-index-c-title {
    display: block;
    padding: .1rem 0 .35rem;
}

.gn-index td.gn-index-c-title::before {
    display: none;
}

/* ── the title cell ───────────────────────────────────────────────────────
   Thumb and text side by side. Flex is safe here in a way it was not in the subgrid
   list: a table cell aligns on `vertical-align: top`, so nothing hands its baseline to
   the picture. */
.gn-index-t {
    display: flex;
    gap: .6rem;
    align-items: flex-start;
}

.gn-index-thumb {
    flex: none;
    inline-size: 44px;
    block-size: 44px;
    background: var(--gn-surface-2);
    overflow: hidden;
}

.gn-index-thumb img {
    inline-size: 44px;
    block-size: 44px;
    object-fit: cover;
}

/* No cover image. A dashed empty box says "missing" and nothing else; this says who is
   at fault. No letter-spacing on this one — "SOMEONE" is 7 characters and 44px is all
   there is. */
.gn-index-thumb-none {
    display: grid;
    place-items: center;
    padding: 1px;
    border: 1px dashed var(--gn-border-strong);
    font-size: var(--gn-fs-2xs);
    line-height: 1.1;
    text-align: center;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

.gn-index-t-main {
    min-width: 0;
}

/* The category (BlogPost.Categories), not a tag: one top theme per article, and the
   accent is what makes it read as a section marker rather than another chip. */
.gn-index-kick {
    display: block;
    margin-block-end: 1px;
    font-size: var(--gn-fs-2xs);
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--gn-accent);
}

.gn-index-link {
    display: inline-block;
    font-size: var(--gn-fs-md);
    font-weight: 700;
    line-height: var(--gn-lh-tight);
    letter-spacing: -.01em;
    color: var(--gn-text);
    text-decoration: none;
}

/* --gn-text on hover too: the accent moves to the underline, so the pointer marks one
   row instead of every headline being coloured at rest. */
.gn-index-link:hover,
.gn-index-link:focus-visible {
    color: var(--gn-text);
    text-decoration: underline;
    text-decoration-color: var(--gn-accent);
    text-decoration-thickness: 2px;
    text-underline-offset: .2em;
}

.gn-index-dek {
    display: block;
    margin: .2rem 0 0;
    font-size: var(--gn-fs-xs);
    font-weight: 400;
    letter-spacing: normal;
    line-height: 1.45;
    color: var(--gn-text-muted);
}

/* ── chips ────────────────────────────────────────────────────────────────
   The sub-taxonomy, open-ended where the kicker above is not. */
.gn-index-tags {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
}

/* No "#" prefix — L3 had one, we are not doing hashtags (owner, 2026-08-11). The accent rides
   the chip's LEFT EDGE instead, which is where it belonged anyway: an edge is a non-text
   boundary and owes 3:1, where accent-coloured text this small would owe 4.5:1 and miss it in
   the light palettes. */
.gn-tag {
    font-size: var(--gn-fs-2xs);
    line-height: 1.6;
    letter-spacing: .04em;
    padding: 0 .35rem;
    background: var(--gn-surface-2);
    color: var(--gn-text-muted);
    box-shadow: inset 0 0 0 1px var(--gn-border);
    border-inline-start: 2px solid var(--gn-accent);
    white-space: nowrap;
}

/* ADR-045 §2 rule 11: every state says its own word — READ, UNREAD, DRAFT, SCHEDULED —
   so the fill is emphasis and never the carrier. */
.gn-pill {
    display: inline-block;
    padding: 1px 7px;
    font-size: var(--gn-fs-2xs);
    font-weight: 700;
    letter-spacing: .1em;
    text-transform: uppercase;
    white-space: nowrap;
}

.gn-pill-read {
    background: var(--gn-accent);
    color: var(--gn-on-accent);
}

/* Opened, not finished. --gn-accent-2 is the one thing it is for (css-tokens.md): the second
   hue exists to pull an eye to a single item, used sparingly, and there is at most a handful of
   half-read articles in a list. Its own ink token, because --gn-on-accent is tuned for the
   PRIMARY accent and the two fills are 60° apart. */
.gn-pill-started {
    background: var(--gn-accent-2);
    color: var(--gn-on-accent-2);
}

/* A status label, not a control: the word carries the state, so the chip edge is
   decorative and may sit on the hairline token. */
.gn-pill-quiet {
    background: var(--gn-surface-2);
    color: var(--gn-text);
    box-shadow: inset 0 0 0 1px var(--gn-border);
}

/* Taken down (/blog/admin). The one pill that should catch an eye crossing the table:
   a hidden article is a state somebody chose in a hurry and may have forgotten. */
.gn-pill-alarm {
    background: var(--gn-danger);
    color: var(--gn-on-accent);
}

/* Edit beside Hide. The form is a block, so without this the button drops below the
   link and the Action column doubles in height on every row. */
.gn-index-actions {
    display: flex;
    align-items: center;
    gap: var(--gn-space-1);
}

.gn-index-num {
    font-variant-numeric: tabular-nums;
}

/* The release time under a Pending pill (/blog/admin). Under it rather than inside: a pill that
   grows a timestamp stops being scannable as a state. */
.gn-index-when {
    display: block;
    margin-block-start: 2px;
    font-size: var(--gn-fs-2xs);
    color: var(--gn-text-muted);
    font-variant-numeric: tabular-nums;
}

/* The admin table's Edit button. A .btn-sm is under the 44px floor, and css-tokens.md's touch
   rule applies to admin too — so grow it on the coarse pointers that need it and leave the dense
   version alone everywhere else. The predicate is the one the head script writes; never re-derive
   it from a media query. */
[data-gn-input="coarse"] .gn-index-c-action .btn {
    min-block-size: var(--gn-touch);
    display: inline-flex;
    align-items: center;
}

.gn-index-comments {
    display: inline-flex;
    align-items: baseline;
    gap: .35em;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* The C3PO mark sits in the last column, so its hover card's default `left: 0` hangs
   15rem off the right edge of the page and takes the horizontal scrollbar with it.
   The card is the component's; the edge it opens against is this table's problem. */
.gn-index-c-ai .gn-c3-peek {
    left: auto;
    right: 0;
}

/* ── The breakdown as an overlay, on the index ONLY (owner, 2026-08-11) ──────
   ADR-042 §7 is "expand in flow, never overlay", and ADR-045 §5 built this panel to that rule.
   It holds on an article, where the panel has the measure to itself. It fails here: the C3PO
   column is width:1%, so the breakdown rendered into ~150px of column and was unreadable.
   The index takes the exception; every other surface keeps the rule.

   SCOPED, AND DELIBERATELY CHEAP. No markup change, no per-instance id (S4.7 dropped those with
   Bootstrap and they stay dropped), no JS, no render mode — <details> still owns open/closed.

   THE SCRIM IS THE SUMMARY'S OWN ::after, AND THAT IS THE WHOLE TRICK. A pseudo-element belongs
   to the element that owns it for hit-testing, so a click anywhere on the dim IS a click on the
   <summary> — which is the toggle. That buys click-outside-to-close with no JS, no id and no
   dialog element. The first cut had the scrim on the panel with pointer-events:none, which left
   the only way out being a second click on a 16px mark hidden behind the dim: nobody would find
   it (owner, 2026-08-11 — "how the hell does it close?").

   Escape is the one part CSS cannot do; shell.js's existing Escape handler closes this before it
   considers the drawer. Still no focus trap and no scroll lock, so it is a light-dismiss popup
   rather than a true modal — which is the honest description of what a <details> can be.
   THE UPGRADE, when this is revisited: the [popover] attribute, already used by .gn-dafuq-panel
   (row S3.8). It gets light-dismiss, Escape and the top layer natively. Its cost is the
   per-instance id, which is why it is not here yet. See §3a's z-stack note.

   Z-STACK: both numbers below are bands §3a already documents, not new ones. 1050 is the scrim
   band and 1150 is #gn-win's — the win screen is raised only during a game and the blog index
   never has one, so the two can never be on screen together. */
.gn-index-c-ai .gn-c3-panel[open] .gn-c3-summary::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 1050;
    background: rgba(0, 0, 0, .55);
    cursor: default;   /* the mark's pointer would promise the dim was itself a control */
}

/* Positioned so the ::after above has a stacking context to sit in; the panel below shares this
   z-index and comes later in the DOM, so it paints over the scrim rather than under it. */
.gn-index-c-ai .gn-c3-panel[open] .gn-c3-summary {
    position: relative;
    z-index: 1150;
}

/* An OPACITY BELOW 1 CREATES A STACKING CONTEXT, so on a read row (60%, above) the fixed panel
   would be trapped inside the row: painted at 60% and underneath the top bar, with its own
   z-index meaningless. Dropping the fade while the breakdown is open removes the context — and
   un-fading the row you are inspecting is what you would want anyway. */
.gn-index tbody tr:has(.gn-c3-panel[open]) {
    opacity: 1;
}

.gn-index-c-ai .gn-c3-panel[open] .gn-c3-body {
    position: fixed;
    z-index: 1150;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    transform: translate(-50%, -50%);
    inline-size: min(52ch, 92vw);
    /* The one inner scroller on the site besides the drawer body (ADR-042 §7). A three-phase
       breakdown with full notes can pass 80dvh, and a panel that cannot reach its own last line
       is worse than the rule it breaks. */
    max-block-size: 80dvh;
    overflow-y: auto;
    padding: var(--gn-space-3);
    background: var(--gn-surface);
    border: 1px solid var(--gn-accent);
    border-radius: var(--gn-radius);
    box-shadow: var(--gn-shadow);
    /* The cell's own alignment and the peek's nowrap must not follow it out here. */
    text-align: left;
    white-space: normal;
}

/* ── md and up: become a real table ───────────────────────────────────────
   min-width-only ladder (css-tokens.md) ⇒ the visually-hidden thead has to be unwound
   here rather than scoped away in a max-width query. */
@media (min-width: 768px) {
    .gn-index {
        display: table;
    }

    .gn-index thead {
        display: table-header-group;
        position: static;
        width: auto;
        height: auto;
        overflow: visible;
        clip: auto;
        white-space: normal;
    }

    .gn-index tbody {
        display: table-row-group;
    }

    /* EVERY row, not `tbody tr`. The header's <tr> is left at `display: block` by a tbody-scoped
       reset, and a block <tr> full of table-cells gets wrapped in an anonymous table of its own:
       the header lays out at its text widths in its own little grid while the body lays out the
       real columns, and the two rows share nothing. It looks exactly like a misaligned header,
       which sends you hunting for a width bug that is not there. */
    .gn-index tr {
        display: table-row;
    }

    /* Undoing the card look needs the SAME selector the card rule used. `.gn-index tr` is (0,1,1)
       and `.gn-index tbody tr` is (0,1,2), so a narrower-looking reset silently loses every
       property to it: the rows kept the card's border and, worse, its --gn-surface background,
       which is what the zebra stripe below was being drawn against. Even rows were the card
       surface and odd rows were the stripe, so the two alternated between two tints that are
       eight values apart and the striping read as "no striping". */
    .gn-index tbody tr {
        padding: 0;
        margin: 0;
        border: 0;
        background: none;
    }

    .gn-index td,
    .gn-index th {
        display: table-cell;
    }

    .gn-index td::before {
        display: none;
    }

    .gn-index th {
        padding: .35rem .7rem;
        border-block-end: 2px solid var(--gn-accent);
        font-size: var(--gn-fs-2xs);
        font-weight: 700;
        letter-spacing: .16em;
        text-transform: uppercase;
        color: var(--gn-text-muted);
        white-space: nowrap;
    }

    .gn-index td {
        padding: .5rem .7rem;
        border-block-end: 1px solid var(--gn-border);
        vertical-align: top;
        font-size: var(--gn-fs-sm);
        /* L3's rhythm, not the site's --gn-lh (1.6). A grid row is a line of data, not a passage
           of prose, and the reading leading adds ~3px per line to every row in the index. */
        line-height: 1.5;
    }

    /* Mixed off --gn-text rather than set to --gn-surface (which is what L3 does). A surface is
       only 8 values off the ground in the default palette and closer than that in others, so the
       stripe was landing somewhere between invisible and absent depending on the theme. A mix of
       the ink is guaranteed to have somewhere to go on any of the twelve, light or dark, because
       the ink is the one colour that must always contrast with the ground. */
    .gn-index tbody tr:nth-child(odd) {
        background: color-mix(in srgb, var(--gn-text) 4%, transparent);
    }

    .gn-index tbody tr:hover,
    .gn-index tbody tr:focus-within {
        background: var(--gn-surface-2);
    }

    /* Read articles recede so the eye lands on the unread ones (owner, 2026-08-11). :has() rather
       than a data- attribute because the pill already states the fact and two sources for one
       truth is one too many. Hover puts it back: a faded row is a hint, not a penalty, and the
       row you are pointing at is the one you want readable. */
    .gn-index tbody tr:has(.gn-pill-read) {
        opacity: .6;
    }

    .gn-index tbody tr:has(.gn-pill-read):hover,
    .gn-index tbody tr:has(.gn-pill-read):focus-within {
        opacity: 1;
    }

    .gn-index td:first-child,
    .gn-index th:first-child {
        padding-inline-start: 0;
    }

    .gn-index td:last-child,
    .gn-index th:last-child {
        padding-inline-end: 0;
        text-align: right;
    }

    .gn-index td.gn-index-c-title {
        display: table-cell;
        padding: .5rem .7rem;
    }

    /* Everything except the title is content-sized: width:1% collapses a table cell to
       its content, which is the table equivalent of max-content and needs no px guess. */
    .gn-index-c-date {
        width: 1%;
        white-space: nowrap;
        color: var(--gn-text-muted);
    }

    .gn-index-c-tags,
    .gn-index-c-ai {
        width: 1%;
    }

    .gn-index-c-talk,
    .gn-index-c-status,
    .gn-index-c-state,
    .gn-index-c-action {
        width: 1%;
        white-space: nowrap;
    }

    /* Dense at md; the dek returns at xl, where there is room for it. */
    .gn-index-dek {
        display: none;
    }
}

@media (min-width: 1200px) {
    .gn-index-dek {
        display: block;
        max-inline-size: 62ch;
    }
}

/* ── The console tab strip — row S4.6 ───────────────────────────────────────
   One hub per area (account · site admin · blog admin), ONE ROUTE PER TAB. The tabs are
   plain anchors, not `role="tablist"`: a real tab widget owes arrow-key roving focus and
   aria-selected, and it can only pay that with a render mode — which ADR-042 §5 forbids
   on chrome. Anchors to real routes get bookmarking, back, and middle-click for free,
   and they survive the form POST every one of these pages ends in. So: no roles, no
   script, `aria-current="page"` on the active one, and the strip is a <nav>.

   WRAPS, never scrolls (ADR-042 §7 — the document is the only scroller besides the
   drawer body). Six blog-admin tabs will take two lines on a phone; two lines of tabs is
   not a defect, an inner scrollport is.

   The type is the console's micro-cap step, same as the column heads and the section
   head it sits above, so a tab and a <th> read as the same register. */
.gn-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--gn-touch-gap);
    margin-block: var(--gn-space-3) var(--gn-space-4);
    border-block-end: 1px solid var(--gn-border);
}

/* 44px of target, and the ink stays small (css-tokens). The -1px pulls the active
   underline down onto the strip's own rule so the two are one line, not two. */
.gn-tabs a {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--gn-touch);
    padding-inline: var(--gn-space-2);
    margin-block-end: -1px;
    border-block-end: 2px solid transparent;
    font-size: var(--gn-fs-2xs);
    font-weight: 700;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
    text-decoration: none;
}

.gn-tabs a:hover {
    color: var(--gn-text);
    border-block-end-color: var(--gn-border-strong);
}

/* NavLink writes .active; aria-current is what a screen reader gets. Both are matched so
   the state is never carried by one of them alone — and never by colour alone (§2 rule
   11), which is what the underline is for. */
.gn-tabs a.active,
.gn-tabs a[aria-current="page"] {
    color: var(--gn-text);
    border-block-end-color: var(--gn-accent);
}

/* AN ADMIN TABLE IS FULL OF UNBREAKABLE TOKENS — email addresses, IP addresses, provider
   keys — and a table column cannot shrink below its longest one. Found by building /admin:
   the users table wanted 1304px inside a 1265px wrap and put HORIZONTAL SCROLL ON THE PAGE,
   which css-tokens forbids outright. It is on every .gn-index because the reader's index has
   the same failure mode waiting in a long unhyphenated title, and `anywhere` breaks a word
   ONLY when the alternative is overflow — so nothing that fits today changes. */
.gn-index td {
    overflow-wrap: anywhere;
}

/* The admin-only column shape. Inside the md query because in card mode a cell is a
   full-width block and the number means nothing there. min-width paired with
   max-inline-size, so this is a proportion with a floor rather than a fixed pixel width on
   a reader-facing surface (css-tokens). Without it the two Reason inputs collapse to ~20px:
   they are flex children of a `d-flex` row whose other child is a fixed-width button. */
@media (min-width: 768px) {
    .gn-index td.gn-admin-mod {
        min-inline-size: 16rem;
        max-inline-size: 100%;
    }
}

/* Ban / Unban and Total ban / Lift swap labels of very different lengths, and an
   unequal pair of buttons makes two rows of the table look misaligned when they are not. */
.gn-admin-act {
    inline-size: 6.5rem;
    max-inline-size: 100%;
    flex: none;
}

/* A user-agent string is one unbroken token that will blow a table column open. Clamped in
   ch (§7's rule — a px cap becomes a different number of characters in every font on
   ADR-044's list) and wrapped rather than truncated: a truncated UA is useless, and a
   `text-overflow` on a table cell needs a fixed layout the rest of the index does not use. */
.gn-admin-ua {
    max-inline-size: 42ch;
    overflow-wrap: anywhere;
    color: var(--gn-text-muted);
}

/* A prose cell in a data table — a language mode's rules, a theme mode's description. Capped
   at a short measure in ch so it stays one column rather than dictating the table's width,
   and quiet so it does not compete with the key beside it. Replaces three `text-muted small`
   + inline `max-width: 320px` pairings on the persona admin. */
.gn-admin-blurb {
    max-inline-size: 40ch;
    color: var(--gn-text-muted);
}

/* ── Trophy tile — ADR-045 §7 (row S3.4) ────────────────────────────────────
   Drawn by TrophyTile.razor. Three states on one element, switched by data-gn-state
   rather than an is-/has- class, so a server-rendered page sets it without building a
   class string (ADR-045 §1).

   NO :hover / :focus-visible / disabled block (§2 rule 9), and that is a decision, not
   an omission: the tile is not a control. The trophy case S4.4 builds may well wrap it
   in one — that wrapper owns the interactive states and the 44px target, because a
   focus ring belongs on the thing that takes focus, not on a box inside it.

   §2 rule 11 — the three states are never told apart by colour alone:
     hidden    no plate,     quiet dashed border,  glyph at --gn-text-muted
     revealed  no plate,     solid border,         glyph at --gn-border-strong
     earned    accent plate, solid accent border,  glyph at --gn-on-accent
   Plate presence and border style carry it visually; TrophyTile also writes a
   visually-hidden "earned" / "not yet earned" so the distinction survives with no
   styling at all. */
.gn-trophy-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--gn-space-1);
    padding: var(--gn-space-3);
    text-align: center;
    background: var(--gn-surface);
    border: 1px solid var(--gn-border);
    border-radius: var(--gn-radius);
}

.gn-trophy-tile[data-gn-state="hidden"] {
    border-style: dashed;
}

/* data-gn-tier is emitted and DELIBERATELY UNSTYLED (row S4.4, owner 2026-08-11). Tier is a rank
   where higher is worth more; every trophy is 5 today and may stay there forever, but nothing is
   allowed to assume a single tier. When the catalog is graded, the tier's look is rules here —
   no change to TrophyTile, TrophyCase or TrophyDefinition. A hidden tile carries no tier at all
   (see TrophyTile's leak boundary), so any rule added here must read as a default without one. */

/* The one asset, sized off the type scale so the tile grows with the reader's setting. */
.gn-trophy-tile-art {
    display: grid;
    place-items: center;
    inline-size: 3em;
    block-size: 3em;
    font-size: var(--gn-fs-2xl);
    border-radius: 50%;
    color: var(--gn-border-strong);
}

/* The glyph's share of the plate, and it is a RATIO rather than a scale step on purpose — the
   plate is already 3em of --gn-fs-2xl, so a rem here would break the "the tile grows with the
   reader's type setting" line above. At the inherited 1em the trophy sat at a third of the
   circle and read as a speck (owner, 2026-08-11); 1.6 of 3 fills it without touching the rim. */
.gn-trophy-tile-art > i {
    font-size: 1.6em;
    line-height: 1;
}

.gn-trophy-tile[data-gn-state="hidden"] .gn-trophy-tile-art {
    color: var(--gn-text-muted);
}

/* Earned: the same glyph on the accent plate. --gn-on-accent is the token held to 4.5:1
   against every fill by palette-contrast.py, so this is the one pairing that is already
   proven across all twelve palettes. */
.gn-trophy-tile[data-gn-state="earned"] {
    border-color: var(--gn-accent);
}

.gn-trophy-tile[data-gn-state="earned"] .gn-trophy-tile-art {
    background: var(--gn-accent);
    color: var(--gn-on-accent);
}

.gn-trophy-tile-title {
    margin: 0;
    font-weight: 600;
}

.gn-trophy-tile-condition,
.gn-trophy-tile-rarity {
    margin: 0;
    font-size: var(--gn-fs-sm);
    color: var(--gn-text-muted);
}

.gn-trophy-tile-rarity {
    font-style: italic;
}

/* The ring and its count read as one phrase, so they share a line and never wrap apart. */
.gn-trophy-tile-progress {
    display: flex;
    align-items: center;
    gap: var(--gn-space-1);
    margin: 0;
    font-size: var(--gn-fs-sm);
    font-variant-numeric: tabular-nums;
}

/* ── In-flow expanding panel — ADR-045 §5 (row S3.3) ────────────────────────
   The general replacement for what is a modal today. Markup contract:

     <details class="gn-panel" name="…optional group…">
       <summary class="gn-panel-summary">What is behind this</summary>
       <div class="gn-panel-body">…</div>
     </details>

   <details>, NOT THE CHECKBOX HACK. The checkbox-and-<label for> pattern exists in this
   file for exactly one reason: ADR-042 §3's drawer has its trigger in the bar and its
   panel elsewhere in the DOM, so no single element can contain both. Everywhere the
   trigger CAN sit with its panel — every case in this set — <details> gives announced
   expanded state, keyboard operation and Esc semantics with nothing written, and `name=`
   gives exclusive-open accordion behaviour natively with no script.

   THE PANEL IS position: static AND THE DOCUMENT GROWS (ADR-042 §7). No focus trap, no
   aria-modal, no scroll lock, no render mode, no z-index — it is not in the z-stack §3a
   documents because it is not chrome.

   THE MARKER IS THE BROWSER'S. list-style is deliberately untouched: the native
   disclosure triangle already changes SHAPE on open, which is §2 rule 11 satisfied for
   free, and it inherits currentColor so it is correct in all twelve palettes with no
   token spend. Do not replace it with a glyph.

   THE HEIGHT ANIMATION IS THE ONLY REASON ::details-content APPEARS HERE.
   interpolate-size is what makes `auto` an interpolable keyword; without it the
   transition has no end value and simply snaps. content-visibility must transition
   `allow-discrete` or the closed content stays hidden for the whole duration and the
   panel animates an empty box. Where ::details-content is unsupported the whole block is
   dropped and <details> snaps open natively — degrade, never break (§2 rule 9's spirit,
   same as §4's @supports).

   PADDING LIVES ON .gn-panel-body, NOT ON ::details-content, for that same reason: the
   pseudo is the newest part of the platform here and the content must not lose its inset
   on an engine that has not shipped it. overflow: hidden also makes the pseudo a BFC, so
   the body's first and last child margins stay inside instead of collapsing out and
   leaving a gap above a closed panel.

   NO DISABLED STATE, and that is a decision rather than an omission (§2 rule 9): there is
   no disabled attribute on <details>, CSS cannot stop a summary being activated, and a
   panel whose content must not be reached is a panel that is not rendered. Locked/earned
   does not apply — that is the trophy tile.

   KNOWN INTERACTION WITH THE GAMES, recorded rather than fixed (ADR-045 §5): a closed
   panel's content has a 0×0 rect (js.md's trap), so its words are not in the level, and
   opening it later does not re-extract. Acceptable — a closed panel is not prose the
   reader is reading — but an author who habitually collapses half an article changes the
   level composition by doing it.

   Rung 1: a track of class names and no markup of ours, no script, no render mode. */
.gn-panel {
    interpolate-size: allow-keywords;
    border: 1px solid var(--gn-border-strong);   /* §2 rule 3 — a control boundary, not a separator */
    border-radius: var(--gn-radius);
    background: var(--gn-surface);
}

.gn-panel-summary {
    /* The touch target (§2 rule 8). min-block-size rather than a padding guess, and the
       summary stays display: list-item — flex or grid on a summary drops the marker in
       Chrome, which is how this component loses its open/closed shape. */
    min-block-size: var(--gn-touch);
    padding: var(--gn-space-2) var(--gn-space-3);
    cursor: pointer;
    font-weight: 600;
}

.gn-panel-summary:hover {
    background: var(--gn-surface-2);
}

.gn-panel-summary:focus-visible {
    outline: 2px solid var(--gn-focus-ring);
    outline-offset: -2px;   /* inset: the summary sits flush inside the panel border */
}

.gn-panel-body {
    padding: 0 var(--gn-space-3) var(--gn-space-3);
}

.gn-panel::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size .25s ease, content-visibility .25s allow-discrete;
}

.gn-panel[open]::details-content {
    block-size: auto;
}

@media (prefers-reduced-motion: reduce) {
    .gn-panel::details-content {
        transition: none;
    }
}

/* The one variant so far: the ban notice (ADR-020 §2). --gn-danger on the boundary rather than a
   tinted fill — twelve palettes and a tinted ground behind body text is twelve contrast checks
   for one panel. The icon in the summary is the second, non-colour signal (§2 rule 11). */
.gn-panel-danger {
    border-color: var(--gn-danger);
}

/* ── DAFUQ — ADR-045 §10 (row S3.8) ─────────────────────────────────────────
   Descriptive Aside of Frequently Unanswered Questions. A jargon term glows in the prose
   and its explanation opens over it. Markup is GENERATED by Dafuq.cs from what the author
   wrote in the post — nothing here is hand-authored into a page:

     <button class="gn-dafuq" data-gn-loud popovertarget="gn-q-1"
             style="anchor-name:--gn-q-1">term<i class="bi bi-search"></i></button>
     …later, at the end of the article…
     <div popover id="gn-q-1" class="gn-dafuq-panel" data-gn-noscan
          style="position-anchor:--gn-q-1">…any HTML the author wrote…</div>

   THE GLOW IS NOT THE ONLY SIGNAL (§2 rule 11). Colour alone fails on some of the twelve
   palettes and on every reader who cannot tell the pair apart, so every mark also carries
   a magnifier glyph. The glyph is what says "these words do something"; the glow only says
   how badly you probably want it.

   LOUD vs QUIET IS THE READER'S JARGON LEVEL (owner, 2026-08-10). data-gn-loud is stamped
   server-side when the term's grade exceeds the reader's own level. Quiet is still visible
   — a fluent reader can see that an aside exists — and BOTH open. Nothing is withheld from
   anyone; §10's whole argument for leaving this ungated applies to the grade too.

   TOUCH TARGET: this is the one component in the set that cannot meet §2 rule 8, and it is
   a spec exemption rather than a miss. The target is a run of words inside a sentence, and
   growing it to 44px would either break the line box or overlap the lines above and below.
   WCAG 2.5.8 excludes inline targets in a block of text for exactly this reason.

   THE TRIGGER CANNOT WRAP MID-TERM, and that is the platform's call rather than ours.
   `display: inline` on a <button> is accepted as a specified value and thrown away as a used
   one — every engine display-normalizes a form control back to inline-block (measured in the
   browser pane, 2026-08-10, not assumed). So a multi-word term moves whole to the next line
   instead of breaking across it. Accepted; the alternative is giving up <button>, and with it
   popovertarget and the entire no-script story. max-inline-size is the part that matters: it
   is what stops a long term running past the measure into horizontal page scroll, by wrapping
   inside the button instead. */
.gn-dafuq {
    max-inline-size: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: inherit;
    cursor: help;
    /* The quiet state. em, not px: the halo tracks the reader's type step (§2 rule 8). */
    text-shadow: 0 0 .3em var(--gn-dafuq-glow);
}

.gn-dafuq[data-gn-loud] {
    text-shadow: 0 0 .25em var(--gn-dafuq-glow), 0 0 .7em var(--gn-dafuq-glow);
}

/* The non-colour half of the cue. aria-hidden in the generated markup — the term itself is
   the button's accessible name, and "search" read out mid-sentence is noise. */
.gn-dafuq .bi {
    font-size: .7em;
    margin-inline-start: .15em;
    vertical-align: .15em;
}

.gn-dafuq:hover,
.gn-dafuq:focus-visible {
    text-shadow: 0 0 .25em var(--gn-dafuq-glow), 0 0 .9em var(--gn-dafuq-glow);
}

.gn-dafuq:focus-visible {
    outline: 2px solid var(--gn-focus-ring);
    outline-offset: 2px;
}

/* No disabled state and no locked state, both deliberate (§2 rule 9). §10 puts this
   permanently ungated: it exists to equalise readers of different education levels, and a
   locked explanation withholds understanding from exactly the readers it is for. */

/* THE FALLBACK IS WRITTEN FIRST, and that ordering is the point of it (§10: "build and
   check the fallback path first, not last"). Without anchor positioning an unpositioned
   popover renders in the TOP-LEFT CORNER of the viewport — not near the word, not
   obviously broken, just wrong. These rules are what a browser without it gets. */
.gn-dafuq-panel {
    position: fixed;
    /* A sheet a gutter in from each edge, sitting ABOVE the strip rather than under it — the
       strip is fixed at the bottom at z 1000 and this is in the top layer, so without the
       clearance the sheet would cover the readout. inline-size: auto is not redundant: the UA
       stylesheet gives every [popover] width: fit-content, which would otherwise beat the two
       insets and leave the sheet hugging one corner. */
    inset: auto var(--gn-gut) calc(var(--gn-strip-h) + var(--gn-gut) + env(safe-area-inset-bottom)) var(--gn-gut);
    inline-size: auto;
    margin: 0;
    max-block-size: 60dvh;
    overflow: auto;
    padding: var(--gn-space-3);
    border: 1px solid var(--gn-border-strong);   /* §2 rule 3 — a control boundary */
    border-radius: var(--gn-radius);
    background: var(--gn-surface);
    color: var(--gn-text);
    font-size: var(--gn-fs-sm);
}

/* THE SECOND RECORDED EXCEPTION IN THIS COMPONENT, after §10's overlay. §2 rule 7 says the
   document is the scroller and no component gets an inner one — but a popover is in the top
   layer, so the document scroller is not available to it by construction, and the aside now
   holds author HTML: images, video, a list of links. The cap plus a scroller is what keeps a
   tall aside reachable at all. Media is capped too, so this is a safety valve rather than
   the normal case. */
.gn-dafuq-panel :is(img, video, iframe) {
    max-inline-size: 100%;
    block-size: auto;
}

.gn-dafuq-panel > :last-child {
    margin-block-end: 0;
}

@supports (anchor-name: --a) {
    .gn-dafuq-panel {
        position: absolute;
        /* Under the word by preference; flip above when there is no room below. The reader is
           looking at the term, so the aside belongs on the line they are already reading from. */
        position-area: block-end span-inline-end;
        position-try-fallbacks: flip-block;
        inset: auto;
        margin-block-start: var(--gn-space-1);
        inline-size: min(44ch, calc(100vw - 2 * var(--gn-gut)));
    }
}

/* ── Palette picker — /Account/Manage/Appearance ────────────────────────────
   A LIST OF RADIOS, NOT A <select>. A name in a dropdown does not tell you what a palette
   looks like, and a <select> cannot hold markup in an <option> — so the swatches could
   never live inside one. A radio list puts them in the control itself: nothing to open,
   nothing covering the examples you are comparing against, and keyboard arrow-navigation,
   the form post and the disabled state all come from the platform.

   Each row opts into one palette with [data-gn-palette="…"] (§1b's second selector), so
   every chip is a plain var(--gn-*) that resolves to THAT palette's value. No hex is
   repeated here, and a retuned palette retunes its swatches with it — the failure mode a
   hand-written legend would have had.

   THE ROW PAINTS ITS OWN GROUND, because a palette is its colours ON its ground: the same
   accent reads completely differently on paper and on charcoal, and chips floating on the
   current page's background would be a lie about half the catalogue.

   NO --gn-bg CHIP. It would be a chip the exact colour of the row it sits in, which is an
   empty box — the row IS the background swatch.

   NO "DEFAULT" ROW EITHER, for the same reason inverted: it was the one row with no palette
   to paint. The default is a palette in the list, so .gn-swatch-badge marks it there.

   Selection is the RADIO DOT first (§2 rule 11): the border is reinforcement, and it has to
   be, because on twelve different palettes a border colour alone is not a reliable signal.
   TWO COLOURS ON PURPOSE (owner, 2026-08-10) — the dot is --gn-accent-2 (from the site-wide
   accent-color rule above §2, which every selection control gets), the border is --gn-accent.
   The row already carries six chips of the palette it is advertising; painting its frame in
   the second hue too made the row's own sample harder to read against the frame. */
.gn-swatch-row {
    display: flex;
    align-items: center;
    gap: var(--gn-space-2);
    /* The touch target (ADR-045 §2 rule 8) — a whole row, not just the radio. */
    min-block-size: var(--gn-touch);
    padding: var(--gn-space-1) var(--gn-space-2);
    border: 1px solid var(--gn-border-strong);
    border-radius: var(--gn-radius);
    background: var(--gn-bg);
    color: var(--gn-text);
    font-size: var(--gn-fs-sm);
    cursor: pointer;
}

.gn-swatch-row + .gn-swatch-row {
    margin-block-start: var(--gn-touch-gap);
}

.gn-swatch-row:has(:checked) {
    border-color: var(--gn-accent);
    border-width: 2px;
    /* Keeps the row from growing 2px and nudging the list when selection moves. */
    padding: calc(var(--gn-space-1) - 1px) calc(var(--gn-space-2) - 1px);
}

.gn-swatch-row:has(:focus-visible) {
    outline: 2px solid var(--gn-focus-ring);
    outline-offset: 2px;
}

/* Locked (no persona-manual trophy). NOT dimmed: opacity on a swatch row would misreport
   every colour in it, and this list's whole job is reporting colours honestly. The cursor
   says it, and TrophyGateNotice above says it in words. */
.gn-swatch-row:has(:disabled) {
    cursor: not-allowed;
}

.gn-swatch-radio {
    flex: 0 0 auto;
}

.gn-swatch-name {
    flex: 0 0 auto;
    font-weight: 600;
}

/* Marks the palette you get when you pick nothing — one per ground. Tokens only, so it reads
   on all twelve grounds it can land on; --gn-surface-2 is the one fill guaranteed to differ
   from the row's own --gn-bg in every palette. */
.gn-swatch-badge {
    flex: 0 0 auto;
    padding: 0 var(--gn-space-1);
    border: 1px solid var(--gn-border);
    border-radius: var(--gn-radius);
    background: var(--gn-surface-2);
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-xs);
    text-transform: uppercase;
    letter-spacing: .05em;
}

.gn-swatch-desc {
    flex: 1 1 auto;
    min-inline-size: 0;
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-xs);
}

/* The ground headings between the two groups — ADR-044 §2's advice, not a restriction. */
.gn-swatch-group {
    margin: var(--gn-space-3) 0 var(--gn-space-1);
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-xs);
    text-transform: uppercase;
    letter-spacing: .05em;
}

.gn-swatch {
    flex: 0 0 auto;
    /* em, so the strip scales with the reader's type step like everything else. */
    inline-size: 1.4em;
    block-size: 1.4em;
    border: 1px solid var(--gn-border-strong);
    border-radius: var(--gn-radius);
}

.gn-swatch-surface { background: var(--gn-surface); }
.gn-swatch-text { background: var(--gn-text); }
.gn-swatch-accent { background: var(--gn-accent); }
.gn-swatch-accent2 { background: var(--gn-accent-2); }
.gn-swatch-link { background: var(--gn-link); }
.gn-swatch-code { background: var(--gn-code); }

/* ── Persona survey, one question at a time — A1 (row S4.3) ─────────────────
   design-explore/interactions.html §A1, resolved onto the token scale rather than copied as
   literals. The markup is the same static-SSR form it always was; blog.js adds .gn-tf-on and
   hides every fieldset but one, so everything gated on that class is the enhanced view and
   everything outside it also has to look right with JS off, where all 12 questions stack.

   No inner scroller and no overlay (ADR-042 §7): the stage holds a height and swaps its
   contents, which is the whole reason A1 was picked over a paged form. */
.gn-tf-top {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gn-space-3);
    margin-block-end: var(--gn-space-4);
}

/* The same .16em micro-caps as the article kicker and the index column heads — one voice for
   every small label on the site. */
.gn-tf-count {
    font-size: var(--gn-fs-2xs);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
    white-space: nowrap;
}

.gn-tf-bar {
    flex: 1 1 12rem;
    height: 8px;
    overflow: hidden;
    background: var(--gn-surface-2);
    border-radius: var(--gn-radius-sm);
}

.gn-tf-bar > i {
    display: block;
    height: 100%;
    background: var(--gn-accent);
    transition: width .45s cubic-bezier(.22, 1, .36, 1);
}

/* The height is what stops the page jumping as questions of different lengths swap in, and it
   only applies once one question is on screen at a time. */
.gn-tf-on .gn-tf-stage {
    min-height: 22rem;
}

.gn-tf-on .gn-tf-card {
    animation: gnTfIn .32s cubic-bezier(.22, 1, .36, 1) both;
}

/* Going back animates from the other side, so the direction of travel is legible without a
   label. blog.js sets data-dir on the step it reveals. */
.gn-tf-on .gn-tf-card[data-dir="back"] {
    animation-name: gnTfBack;
}

@keyframes gnTfIn {
    from { opacity: 0; transform: translateY(14px); }
}

@keyframes gnTfBack {
    from { opacity: 0; transform: translateY(-14px); }
}

/* With JS off the cards stack, so they need the separation the enhanced view gets from the
   stage. Harmless when only one is visible. */
.gn-tf-card {
    margin-block-end: var(--gn-space-4);
}

/* Bootstrap's reboot floats a legend and gives it a viewport-derived font-size, which is off
   the type scale (css-tokens.md: never write a literal font-size). Both are undone here.

   The measure is --gn-measure, NOT the mockup's 34ch (owner, 2026-08-11). 34ch is a display-type
   convention and it broke a two-line question into seven on a desktop column with room to
   spare — and the options underneath run the full width, so the question was the only thing on
   the page holding itself in. ADR-042 §7's rule already answers this and it is the same token
   the article body uses. */
.gn-tf-q {
    float: none;
    width: auto;
    max-width: min(var(--gn-measure), 100%);
    margin-block-end: var(--gn-space-3);
    padding: 0;
    font-size: var(--gn-fs-2xl);
    font-weight: 600;
    line-height: var(--gn-lh-tight);
    letter-spacing: -.015em;
}

.gn-tf-opts {
    display: grid;
    gap: var(--gn-touch-gap);   /* §8's separation floor, not a decorative gap */
}

/* The whole row is the target. The radio inside it is real and focusable and simply not drawn —
   the number chip is its visual stand-in, which is also the keyboard affordance. */
.gn-tf-opt {
    display: flex;
    align-items: flex-start;
    gap: var(--gn-space-2);
    min-height: var(--gn-touch);
    padding: .6rem .75rem;
    background: var(--gn-bg);
    color: var(--gn-text);
    /* --gn-border-strong, not --gn-border: this row IS the control, and WCAG 1.4.11 holds a
       control boundary to 3:1. */
    border: 1px solid var(--gn-border-strong);
    border-radius: var(--gn-radius);
    cursor: pointer;
    transition: transform .12s ease, border-color .12s ease, background .12s ease;
}

.gn-tf-radio {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    opacity: 0;
}

.gn-tf-opt:hover {
    border-color: var(--gn-accent);
    transform: translateX(3px);
}

.gn-tf-opt:has(.gn-tf-radio:checked) {
    background: var(--gn-accent);
    color: var(--gn-on-accent);
    border-color: var(--gn-accent);
}

/* The ring goes on the row because the input it belongs to is 1px square. */
.gn-tf-opt:has(.gn-tf-radio:focus-visible) {
    outline: 2px solid var(--gn-focus-ring);
    outline-offset: 2px;
}

.gn-tf-key {
    flex: none;
    display: grid;
    place-items: center;
    width: 1.6rem;
    height: 1.6rem;
    background: var(--gn-surface-2);
    color: var(--gn-text-muted);
    border-radius: var(--gn-radius-sm);
    font-size: var(--gn-fs-xs);
    font-weight: 700;
}

/* Selected inverts the chip, so it stays readable on the accent fill it now sits on. */
.gn-tf-opt:has(.gn-tf-radio:checked) .gn-tf-key {
    background: var(--gn-on-accent);
    color: var(--gn-accent);
}

.gn-tf-opt-text {
    line-height: var(--gn-lh);
}

/* ADR-009's trophy-style icon, inline with the option text it belongs to. */
.gn-tf-opt-icon {
    margin-inline-end: var(--gn-space-2);
    vertical-align: text-bottom;
}

.gn-tf-nav,
.gn-tf-submit {
    display: flex;
    flex-wrap: wrap;
    gap: var(--gn-space-2);
}

.gn-tf-nav {
    margin-block-end: var(--gn-space-3);
}

.gn-tf-keys {
    margin-block: var(--gn-space-3);
    font-size: var(--gn-fs-xs);
    color: var(--gn-text-muted);
}

@media (prefers-reduced-motion: reduce) {
    .gn-tf-on .gn-tf-card { animation: none; }
    .gn-tf-bar > i { transition: none; }
    .gn-tf-opt:hover { transform: none; }
}

/* ── Blocked-action confirm — row S4.7 ──────────────────────────────────────
   What replaced the survey's Bootstrap modal. Not a .gn-panel: nobody discloses this, the
   script un-hides it when a submit is intercepted, so there is no summary and nothing to toggle.
   `hidden` is the entire mechanism; this only skins the box.

   --gn-warning on the boundary, and the block carries its own heading text — a reader who cannot
   see the colour still gets "It is only 12 questions!" (§2 rule 11). */
.gn-confirm {
    padding: var(--gn-space-3);
    border: 1px solid var(--gn-warning);
    border-radius: var(--gn-radius);
    background: var(--gn-surface);
}

/* ── Trophy earn strip — ADR-042 §7 (row S4.4) ──────────────────────────────
   The Bootstrap toast this replaces was position: fixed at z-index 1090, which is how it came to
   render inside the game canvas's rectangle mid-run. This is in the document, first child of
   .content, and the page moves down to make room for it.

   ONE strip, however many trophies land at once. S4.7's stand-in was a box per trophy, and three
   clears in a run pushed the article three screens down. The heading counts them and each one is
   a chip on the row below it.

   The chip's art is the TROPHY TILE'S — accent plate, same bi-trophy-fill glyph — because the
   case and the earn moment are the same trophy seen twice, and a .webp here would be ADR-045
   §7's two-files-to-keep-in-sync all over again.

   No animation and no auto-dismiss timer. A six-second toast is unreadable to anyone who looked
   away, and there is nothing here that motion would explain.

   NOT data-gn-noscan: it is static, in document coordinates, and both games extract words there —
   so unlike the sticky pane it is exactly where the reader sees it (§2 rule 6).

   Injected by trophies.js, so it CANNOT be scoped CSS — that is the whole reason this block is
   here and the case's own styles are in TrophyCase.razor.css. */
.gn-earn {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--gn-space-1) var(--gn-space-3);
    padding: var(--gn-space-3);
    margin-bottom: var(--gn-space-3);
    border: 1px solid var(--gn-accent);
    border-left-width: 4px;
    border-radius: var(--gn-radius);
    background: var(--gn-surface);
}

.gn-earn-head {
    margin: 0;
    font-size: var(--gn-fs-sm);
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

/* Wraps to a second line rather than scrolling — zero inner scrollers (ADR-042 §7), and a strip
   that grew a scrollbar to hold two trophies would be worse than the stack it replaced. */
.gn-earn-list {
    grid-column: 1 / -1;
    display: flex;
    flex-wrap: wrap;
    gap: var(--gn-space-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

.gn-earn-item {
    display: flex;
    align-items: center;
    gap: var(--gn-space-2);
}

/* The tile's earned plate, at body scale. --gn-on-accent is the token palette-contrast.py holds
   to 4.5:1 against every fill, so this pairing is already proven across all twelve palettes. */
.gn-earn-art {
    flex: none;
    display: grid;
    place-items: center;
    inline-size: 2em;
    block-size: 2em;
    border-radius: 50%;
    background: var(--gn-accent);
    color: var(--gn-on-accent);
}

/* Same ratio as the tile's glyph, for the same reason — a 1em trophy in a 2em circle is a speck. */
.gn-earn-art > i {
    font-size: 1.15em;
    line-height: 1;
}

.gn-earn-title,
.gn-earn-desc {
    display: block;
}

.gn-earn-title {
    font-weight: 600;
}

.gn-earn-desc {
    font-size: var(--gn-fs-sm);
    color: var(--gn-text-muted);
}

/* Top-right of the strip's grid, and 44px square on every pointer — this one is a real control
   with nothing else to grow (§2 rule 8), unlike the C3 badge whose size the date line dictates. */
.gn-earn-close {
    grid-area: 1 / 2;
    align-self: start;
    min-width: var(--gn-touch);
    min-height: var(--gn-touch);
    background: none;
    border: 0;
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-xl);
    line-height: 1;
    cursor: pointer;
}

.gn-earn-close:hover {
    color: var(--gn-text);
}

.gn-earn-close:focus-visible {
    outline: 2px solid var(--gn-focus-ring);
    outline-offset: 2px;
}

/* ── Shell pages — ADR-042 §7 (row S4.5) ────────────────────────────────────
   Home, What's Happening, Contact, the legal pages, Store, Banned, Nice Try, Choose
   Handle and Request Access. Eleven pages that predate the console and had never been
   given its vocabulary: they were Bootstrap `.row`/`.col`/`.card` feeds with `display-*`
   headings and literal font-sizes, sitting inside a shell that had learned a different
   language everywhere else.

   ALMOST NOTHING NEW IS AUTHORED HERE, and that is the point of the row. The page head
   IS `.gn-article-head` / `-kick` / `-title` / `-dek` (row S4.2), the section head IS
   `.gn-index-head` (row S4.1), the prose column IS `.gn-prose` (row S3.1), the status
   chip IS `.gn-pill` (row S4.1). The names keep the row they were authored in, exactly
   as `.gn-index-head` kept its when Gaslight Alley took it — a second class on the same
   declarations is cheaper than renaming a component and every call site for tidiness.
   Three things below did not exist and could not be borrowed. */

/* THE ASYMMETRIC GAP, and it is the one ADR-042 §7 rule the shell pages were breaking
   outright: row gap ≈ 1.4× column gap, so a card's own text sits closer to itself than
   to the card below it. Bootstrap's `.g-4` is symmetric and cannot express it.

   auto-fit rather than col-12/col-md-6/col-xl-4 triples, because the count is data:
   Home has three cards, "Coming up" has as many as the JSON holds, and a track list that
   reads "at least 18rem" needs no breakpoint to be told what to do at 900px. That also
   removes the .row negative-margin/.content-gutter interaction (css-tokens) from these
   pages entirely — there is no negative margin left to escape the gutter with. */
.gn-feed {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
    column-gap: var(--gn-space-3);
    row-gap: calc(var(--gn-space-3) * 1.4);
    align-items: stretch;
}

/* The demo/teaser cards. .gn-card is the surface; this is what a card CONTAINS, and it
   exists so the pages can drop Bootstrap's .card/.card-body/.card-title/.card-text stack
   (ADR-043 §2 — cards are ours). Column, so `margin-block-start: auto` on a badge or a
   meter pins it to the bottom and a row of cards keeps its footers level. */
.gn-card-body {
    display: flex;
    flex-direction: column;
    gap: var(--gn-space-2);
    padding: var(--gn-space-3);
    height: 100%;
}

/* Card titles take the index's kicker voice rather than an <h5>'s size: on a feed the
   title is a label for the block, and the blocks are what the eye is scanning. */
.gn-card-title {
    margin: 0;
    font-size: var(--gn-fs-lg);
    line-height: var(--gn-lh-tight);
    letter-spacing: -.01em;
}

/* The micro-cap caption over an inline demo chart or a meter. Same .16em caps as the
   index column heads and the article kicker — one voice for every label on the site. */
.gn-card-cap {
    font-size: var(--gn-fs-2xs);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

/* Pins a badge, meter or CTA to the bottom of a .gn-card-body. Replaces `.mt-auto`,
   which is the same declaration wearing Bootstrap's name. */
.gn-card-foot {
    margin-block-start: auto;
}

/* A label-and-status list — /request-access's "Your requests", and the shape any short
   "thing → state" list on a shell page wants. Replaces Bootstrap's .list-group, which
   ADR-043 §2 assigns to us and which was drawing its own borders and radii from a
   --bs-list-group-* block that no longer had anything to do with the console.

   Hairlines between rows, none around the set: the same separator logic as the comment
   tree, and it stops a two-item list reading as a heavyweight boxed component. */
.gn-statuslist {
    list-style: none;
    margin: 0;
    padding: 0;
}

.gn-statuslist li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--gn-space-3);
    padding: var(--gn-space-2) 0;
    border-block-start: 1px solid var(--gn-border);
}

.gn-statuslist li:last-child {
    border-block-end: 1px solid var(--gn-border);
}

/* THE DEAD-END PAGES — /banned and /nice-try. One glyph, one line, no navigation, by
   ADR-020 §5 and ADR-027. Both were built with literal font sizes (12rem, 10rem) and
   Bootstrap's .display-1/.display-2, which is four values sitting outside --gn-fs-root:
   a reader who had turned the type scale down got a full-size finger anyway.

   A MULTIPLE OF THE TOP STEP, not a literal. calc() off --gn-fs-4xl lands within a few
   pixels of what was there and stays inside the reader's control, which is the whole
   reason css-tokens.md bans the literal. The two pages share every rule below so they
   cannot drift — they are one design with two sets of words. */
.gn-slam {
    text-align: center;
    margin-block: var(--gn-space-5);
    padding-block: var(--gn-space-5);
}

.gn-slam-art {
    font-size: calc(var(--gn-fs-4xl) * 4.8);
    line-height: 1;
}

/* /nice-try is an escalation, not the terminal page — it lands one step quieter so the
   final 🖕 in its own ladder still reads as the loudest thing the site has said. */
.gn-slam-art-quiet {
    font-size: calc(var(--gn-fs-4xl) * 4);
}

.gn-slam-title {
    margin-block: var(--gn-space-4) 0;
    font-size: calc(var(--gn-fs-4xl) * 1.8);
    font-weight: 700;
    line-height: var(--gn-lh-tight);
}

.gn-slam-line {
    margin-block: var(--gn-space-3) 0;
    font-size: var(--gn-fs-lg);
    color: var(--gn-text-muted);
}

/* ── 4. App globals ───────────────────────────────────────────────────────── */
.content {
    padding-top: 1.1rem;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid var(--gn-success);
}

.invalid {
    outline: 1px solid var(--gn-danger);
}

.validation-message {
    color: var(--gn-danger);
}

/* Intentionally theme-exempt: must stay readable when the theme is broken. Moved here
   from MainLayout.razor.css in S1.2 (that file is gone). z-index 1200 — above the drawer,
   because an unhandled error has to be reachable whatever else is open. */
#blazor-error-ui {
    color-scheme: light only;
    background: lightyellow;
    color: #212529;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1200;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }

/* Intentionally theme-exempt: the error surface must stay readable even when
   the theme system itself is what broke. */
.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: var(--gn-text-muted);
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* ── Gaslight Alley (ADR-010) ─────────────────────────────────────────────────────────
   Lives here rather than in the module: the blog RCL ships no CSS of its own
   (CONVENTIONS module contract), same as gn-trophy-* and gn-cta. Tokens only, no
   hard-coded colours — these have to survive all twelve palettes on either ground. */

/* The page break between the article and the comment section used to be an <hr
   class="gn-section-break">. Row S4.2 deleted it and both its call sites: the section now
   opens with the shared .gn-comments-head, whose 2px accent rule IS the break, and two
   horizontal rules 40px apart read as a mistake. */

/* The section sits on the article's own measure (.gn-measured in the markup), so the
   composer, the thread and the gate all line up with the prose above them. */
.gn-comments-region {
    margin-block-start: var(--gn-space-5);
}

/* While a game is running (game.js sets body.gn-playing) the comment region is removed from the
   page. That shrinks the document to just the article, so the ship's edge-scroll can't drive down
   into a long thread and comment words are never pulled into the level (ADR-010 §7 amendment).
   Targets the shared class so it hides both the real section and the ADR-011 consent gate. */
body.gn-playing .gn-comments-region {
    display: none;
}

/* The game pans a virtual camera instead of scrolling the page (2026-08-02), so anything still
   visible underneath would sit frozen while the words move over it. Words are already hidden
   one span at a time; media is not, so hide it here. Visibility, not display: removing them
   would reflow the document and move every word rect the level was built from. */
body.gn-playing .content :is(img, svg, video, iframe, figure) {
    visibility: hidden;
}

/* ...and the shell's own furniture, for the same reason once the viewport zooms (owner,
   2026-08-06). The level is drawn at 3x while the chrome stays at 1x, so it reads as a
   different application bolted to the edge of the game. Nothing here is reachable mid-play
   anyway — the canvas is the whole interface.
   Visibility again, not display: the level is built out of the article's word rects, and
   collapsing this would widen the main column and move every one of them.
   S1.2: `.sidebar` and `.gn-footer` no longer exist. The strip is not hidden — under ADR-042
   §4 it BECOMES the game readout, which is S1.3's job; the bar stays because it carries the
   reader bars and sits where a HUD belongs. */
body.gn-playing .gn-legal {
    visibility: hidden;
}

/* ── Reader bars (ADR-013 §7, moved to the header 2026-07-28) ─────────────────────────
   The "as written" notch: where the post sits on the scale before any variant is applied, so a
   reader can see how far they've dragged from the author's own setting. The native way to do this
   is a <datalist> tick, but Bootstrap's .form-range sets appearance:none and browsers then render
   no ticks at all — hence a marker of our own.

   --gn-mark is a 0–1 fraction set inline per post (ReaderBars.razor); the class is only applied
   when the post has been rated, so an unrated post shows nothing rather than a mark at 0. The
   0.5rem/1rem terms are Bootstrap's thumb width: the thumb centre travels from 0.5rem to
   (100% - 0.5rem), never the full track.

   Renamed from .gn-bar in S1.2: ADR-042 §1 names the fixed top bar `.gn-bar`, and the slider
   wrapper held the name first. Three call sites, all renamed together. */
.gn-readerbar {
    position: relative;
    display: inline-flex;
}

.gn-readerbar-marked::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(0.5rem + var(--gn-mark) * (100% - 1rem));
    width: 2px;
    margin-left: -1px;
    background: var(--gn-text);
    opacity: 0.55;
    pointer-events: none;
}

/* ── Comment consent gate (ADR-011) ───────────────────────────────────────────────────
   The blurred area is a placeholder skeleton, not real comments — those aren't rendered
   until the user agrees. The overlay carries the message; the control that opens the terms is
   the <summary> of the .gn-panel below the gate (row S4.7 — a <details> cannot have its trigger
   inside the blurred stack and its panel outside it). */
.gn-gate {
    position: relative;
    border: 1px solid var(--gn-border);
    border-radius: var(--gn-radius, .5rem);
    overflow: hidden;
}

.gn-gate-blur {
    filter: blur(6px);
    pointer-events: none;
    user-select: none;
    padding: 1rem;
}

.gn-gate-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: .5rem;
    padding: 1.5rem;
    /* Sit over the blurred skeleton with a slight scrim so the message stays legible in any theme. */
    background: color-mix(in srgb, var(--gn-bg, #212529) 55%, transparent);
}

.gn-skel-comment {
    padding: .75rem 0;
}

.gn-skel-comment + .gn-skel-comment {
    border-top: 1px solid var(--gn-border);
}

.gn-skel-line {
    height: .7rem;
    border-radius: .25rem;
    background: var(--gn-surface-2);
    margin-bottom: .5rem;
}

.gn-skel-author {
    width: 30%;
    height: .8rem;
    background: var(--gn-border);
}

/* Owner-authored terms text in the consent panel — comfortable reading measure. */
.gn-terms-body {
    line-height: 1.6;
}

/* ── The comment tree — L3 §10 (row S4.2) ─────────────────────────────────────────────
   L3 draws the thread as a hairline grid: `display: grid; gap: 1px` over a --gn-border
   background, so the rules between comments are the background showing through. That
   works for a flat list and NOT for this one — CommentNode recurses, and a nested
   <details> thread cannot be a grid item of its ancestor. The borders below are the same
   look reached the way a tree can reach it.

   The outer box, so the thread reads as one object rather than as loose paragraphs
   trailing off the end of the article. */
.gn-comments-list {
    border: 1px solid var(--gn-border);
    background: var(--gn-surface);
}

.gn-comment {
    padding: var(--gn-space-3);
}

/* The reply rail. Accent rather than the quiet hairline: it is the one line on the page
   that carries meaning (this comment answers that one) rather than just separating two
   things, and at depth it is the only cue that survives. */
.gn-comment-indent {
    margin-left: var(--gn-space-3);
    border-left: 2px solid var(--gn-accent);
    padding-left: var(--gn-space-2);
}

/* The meta line: micro-caps like every other label on the site, with the handle opting
   back out of the caps — a handle is a name the reader chose and shouting it changes
   what it says. */
.gn-comment-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gn-space-2);
    margin-block-end: var(--gn-space-2);
    font-size: var(--gn-fs-2xs);
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

.gn-comment-handle {
    font-size: var(--gn-fs-sm);
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: none;
    color: var(--gn-text);
}

/* Author line breaks are meaningful; the body is plain text, never markdown.
   Long unbroken strings are a comment section's oldest layout attack. */
.gn-comment-body {
    margin: 0;
    font-size: var(--gn-fs-sm);
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.gn-comment + .gn-comment,
.gn-comment-thread > div > .gn-comment {
    border-top: 1px solid var(--gn-border);
}

.gn-comment-thread > summary {
    cursor: pointer;
    padding: 0 var(--gn-space-3) var(--gn-space-2);
    font-size: var(--gn-fs-2xs);
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
    list-style: revert; /* keep the native disclosure triangle; Bootstrap's reboot hides it */
}

/* Action row (vote / reply / edit / delete). flex-wrap so it never clips the last button on an
   indented reply or a narrow screen — a nowrap row silently cut off "Delete" before this. */
.gn-comment-actions {
    row-gap: 0;
}

/* Facebook-style: actions are quiet until you point at them. btn-link underlines by default;
   suppress it and bring it back on hover/focus only. Icon buttons (edit/delete) have no text to
   underline, so this just governs their text siblings. */
.gn-comment-actions .btn-link {
    text-decoration: none;
}

.gn-comment-actions .btn-link:hover,
.gn-comment-actions .btn-link:focus-visible {
    text-decoration: underline;
}

/* Info button with a CSS-only tooltip (content in data-gn-tip). Shows on hover AND focus, so a
   click/tap (which focuses the button) opens it too — no JS, so it survives enhanced nav.
   Click-away or Esc blurs the button and the tip closes itself. Scoped to button so it can't
   bleed into RequestLink.razor's page-scoped span.gn-info (rich-HTML tooltip variant). */
button.gn-info {
    position: relative;
    display: inline-block;
    padding: 0;
    border: 0;
    background: none;
    color: var(--gn-info);
    cursor: pointer;
    line-height: 1;
}

/* Corner variant: adornment at the top-right of a .form-floating input. Lives OUTSIDE the
   floating label (which clips overflow and would cut the tooltip off). */
button.gn-info-corner {
    position: absolute;
    top: .5rem;
    right: .75rem;
    z-index: 5;
}

button.gn-info:hover::after,
button.gn-info:focus::after {
    content: attr(data-gn-tip);
    position: absolute;
    left: 50%;
    bottom: calc(100% + .45rem);
    transform: translateX(-50%);
    z-index: 20;
    width: max-content;
    max-width: 16rem;
    padding: .35rem .6rem;
    font-family: var(--gn-font);
    font-size: var(--gn-fs-sm);
    font-style: normal;
    line-height: 1.35;
    text-align: start;
    white-space: normal;
    color: var(--gn-text);
    background: var(--gn-surface-2);
    border: 1px solid var(--gn-border);
    border-radius: var(--gn-radius-sm);
    box-shadow: 0 2px 8px rgba(0, 0, 0, .4);
}

/* Countdown pie for the comment rate limit (ADR-025 E1). A gn-info button so the tooltip and its
   hover/focus behaviour come for free; --gn-cooldown is the share still to wait, written inline by
   GaslightAlley on each tick, so the wedge drains to nothing as the Post button re-enables.
   AFTER button.gn-info — same specificity, source order is what puts this background back.
   No mask/clip-path here on purpose: either would take the ::after tooltip with it. */
button.gn-cooldown {
    width: 1.15rem;
    height: 1.15rem;
    border-radius: 50%;
    cursor: help;
    background: conic-gradient(var(--gn-accent) var(--gn-cooldown, 0%), var(--gn-surface-2) 0);
}

/* Right-align the corner tooltip with its button so it grows leftward, not off-screen.
   AFTER the generic rule above — same specificity, so source order is what wins here. */
button.gn-info-corner:hover::after,
button.gn-info-corner:focus::after {
    left: auto;
    right: 0;
    transform: none;
}

/* ── Trophy-button nudge ─────────────────────────────────────────────────────
   A new reader has no reason to look at the trophy icon, and the whole
   progression (ADR-013) hangs off noticing it. trophies.js adds .gn-nudge until
   the case has been opened once, then never again on this browser.
   Deliberately not a popup — the owner's call: highlight, don't interrupt. */
@keyframes gn-trophy-nudge {
    0%, 100% { box-shadow: 0 0 0 0 var(--gn-accent); }
    50%      { box-shadow: 0 0 0 .35rem transparent; }
}

.gn-nudge {
    animation: gn-trophy-nudge 1.8s ease-out infinite;
    border-color: var(--gn-accent);
    color: var(--gn-accent);
}

/* Accessibility basic, not an optional polish: a persistent pulse is exactly the
   kind of motion that triggers vestibular symptoms. Keep the emphasis, drop the
   movement. */
@media (prefers-reduced-motion: reduce) {
    .gn-nudge {
        animation: none;
        box-shadow: 0 0 0 .2rem var(--gn-accent);
    }
}

/* ── AI usage label (ADR-040) ────────────────────────────────────────────────
   Three glyphs under the article title and an info button; every word lives in
   the modal. The rings are inline SVG on a 100x100 viewBox, so there is little
   here beyond the plate, the row and the breakdown rows.
   Appended at the end of the file rather than filed into a section above,
   deliberately — a wholesale rewrite of this file was expected, and an append
   conflicts far more cleanly than an insertion. */
/* No plate (owner, 2026-08-07: "the background should be transparent... they should look
   like coloured icons"). The plate was carrying the 3:1 contrast floor — without it the two
   hues fall under it on any ground close to their own hue, and after ADR-044 there are twelve
   grounds rather than one, so the shortfall is no longer confined to a single theme. Accepted
   knowingly: no information is lost, because every ring carries its numbers in a title/aria-label
   and the breakdown states them in text. S2.2 re-measures both hues against all twelve.
   The modal's own rings keep their ground (see .gn-c3-detail below). */
/* Positions the L2 hover card. A <button> cannot contain one, so the badge and its card
   are siblings inside this wrapper. */
.gn-c3-wrap {
    position: relative;
    display: inline-flex;
}

/* L2 (ADR-040 §10): the summary, on hover or keyboard focus. CSS only — the page is static
   SSR and this needs no circuit. Hidden from assistive tech and from touch: the button's
   aria-label already reads the same words, and a coarse pointer has no hover, so a tap goes
   straight to L3 instead of lighting a card up under the finger. */
.gn-c3-peek {
    position: absolute;
    top: calc(100% + var(--gn-space-2));
    left: 0;
    z-index: 20;
    display: none;
    flex-direction: column;
    gap: 2px;
    min-width: 15rem;
    padding: var(--gn-space-2) var(--gn-space-3);
    /* --gn-surface-2, not --gn-c3-plate. The plate is a theme-invariant DARK ground that
       exists so the mark's two hues cannot be tuned away (ADR-040); this card is an ordinary
       tooltip that prints --gn-text on itself, and on a light ground that pairing is
       near-black on near-black. The plate stays where it belongs — under the rings. */
    background: var(--gn-surface-2);
    /* Accent, not the hairline (owner, 2026-08-11). This card floats over arbitrary page content
       — a table row, an article, a zebra stripe — and a --gn-border edge on --gn-surface-2 left
       it reading as part of whatever was behind it. The accent is what says "this is a layer". */
    border: 1px solid var(--gn-accent);
    border-radius: var(--gn-radius);
    color: var(--gn-text);
    font-size: var(--gn-fs-sm);
    line-height: 1.35;
    text-align: left;
    white-space: nowrap;
}

/* The focusable element is the <summary> since row S4.7, not the badge — a descendant selector
   rather than the sibling one this used to be. */
.gn-c3-wrap:hover .gn-c3-peek,
.gn-c3-summary:focus-visible .gn-c3-peek {
    display: flex;
}

/* Once the panel is open the badge has done its job; leaving the hover card floating over the
   breakdown it just revealed is the tooltip covering its own answer. */
.gn-c3-panel[open] .gn-c3-peek {
    display: none;
}

[data-gn-input="coarse"] .gn-c3-peek {
    display: none;
}

/* Accent rule under the title, matching the card's own accent border (owner, 2026-08-11): it
   separates the heading from the three readings, which at this size were running together. */
.gn-c3-peek-head {
    font-weight: 600;
    padding-bottom: var(--gn-space-1);
    margin-bottom: var(--gn-space-1);
    border-block-end: 1px solid var(--gn-accent);
}

.gn-c3-peek-row {
    display: flex;
    align-items: baseline;
    gap: var(--gn-space-2);
}

/* The medium, as its glyph instead of its name. currentColor so it inherits the row's ink and
   needs no token of its own — it is a label, not part of the two-hue mark, so ADR-040's
   theme-invariant --gn-c3-* rule does not reach it. */
.gn-c3-glyph {
    inline-size: 1em;
    block-size: 1em;
    flex: none;
    align-self: center;
    color: var(--gn-text-muted);
}

.gn-c3-peek-phase {
    min-width: 5.5rem;
}

.gn-c3-peek-foot {
    padding-top: var(--gn-space-1);
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-xs);
}

/* ── C3PO badge and its in-flow breakdown — ADR-045 §5 via row S4.7 ──────────
   The breakdown was a Bootstrap modal. It is now a <details> that wraps the badge, so the whole
   thing is one element: .gn-c3-panel > .gn-c3-summary > .gn-c3-wrap > (.gn-c3 + .gn-c3-peek),
   and .gn-c3-body underneath.

   NOT .gn-panel. The generic component (§3, row S3.3) is a bordered, surfaced box with a padded
   summary and a 44px target; this is a bare inline mark that has to sit on an article's date line
   without changing its height. Same MECHANISM, different skin — which is the whole reason ADR-045
   §5 specified a mechanism rather than a class.

   display: block, and every call site puts it in a flex row, so it is a flex item that grows
   downwards when opened. That IS "the document grows" (ADR-042 §7). */
.gn-c3-panel {
    display: block;
}

/* The <summary> is the control. list-style: none because this badge is not a paragraph
   disclosure — the three rings ARE the affordance and a triangle beside them reads as a fourth
   glyph in the set. Open/closed is still carried by shape, not colour (ADR-045 §2 rule 11): the
   breakdown is either under it or it is not. */
.gn-c3-summary {
    display: inline-block;
    list-style: none;
    cursor: pointer;
}

.gn-c3-summary::-webkit-details-marker {
    display: none;
}

.gn-c3-summary:focus-visible {
    outline: 2px solid var(--gn-focus-ring);
    outline-offset: 3px;
}

/* The badge itself: a plain span inside the summary since row S4.7. The button resets stay —
   .gn-c3-bare and the coarse-pointer overlay below both key off this box. */
.gn-c3 {
    display: inline-flex;
    align-items: center;
    gap: var(--gn-space-2);
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

/* The breakdown. Width in ch (css-tokens.md — never a pixel width): a flex item sized by its
   content would otherwise be as wide as the longest note in it, which on an index card's date
   line is the whole card and then some. */
.gn-c3-body {
    width: min(46ch, 100%);
    padding-top: var(--gn-space-2);
}

.gn-c3-body-head {
    margin: 0 0 var(--gn-space-1);
    font-weight: 600;
}

/* Touch target without layout cost. The badge is 32px on an article and 16px on a card,
   both under ADR-028's 44px floor, but growing the button would push the date line down
   (and on the index would make a labelled card taller than an unlabelled one — the exact
   thing the 16px was measured to avoid). An overlay gives the finger 44px and the layout
   nothing. Width needs no help: three rings plus gaps clear 44px at both sizes. */
[data-gn-input="coarse"] .gn-c3 {
    position: relative;
}

[data-gn-input="coarse"] .gn-c3::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 44px;
    transform: translateY(-50%);
}

/* --gn-c3-size is the tuner's Size knob, arriving inline on .gn-c3. A ceiling
   rather than a width: the ring shrinks with its column so a row of them fits
   any viewport with no media query (css-tokens.md, no fixed pixel widths). */
.gn-c3-ring {
    display: block;
    flex: 0 1 var(--gn-c3-size, 44px);
    width: var(--gn-c3-size, 44px);
    max-width: 100%;
    height: auto;
}

/* One phase in the breakdown: its ring, then the numbers and the evidence. */
.gn-c3-detail {
    display: flex;
    align-items: flex-start;
    gap: var(--gn-space-3);
    padding: var(--gn-space-3) 0;
    border-top: 1px solid var(--gn-border);
}

.gn-c3-detail:first-of-type {
    border-top: 0;
    padding-top: 0;
}

/* The breakdown's rings sit on the page, not on the plate, so they carry their own ground the
   same way the badge does. (Said "the modal surface" until row S4.7 put the breakdown in flow —
   the ring's need for its own plate is unchanged, only what is behind it.) */
.gn-c3-detail .gn-c3-ring {
    flex: none;
    width: 3rem;
    background: var(--gn-c3-plate);
    border-radius: 50%;
    padding: 2px;
}

.gn-c3-pct {
    margin-left: var(--gn-space-2);
    color: var(--gn-text-muted);
    font-variant-numeric: tabular-nums;
}

.gn-c3-medium {
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-sm);
}

.gn-c3-note {
    margin: var(--gn-space-2) 0 0;
    font-size: var(--gn-fs-sm);
}

/* The name, explained once at the foot of the breakdown. */
.gn-c3-about {
    margin: var(--gn-space-3) 0 0;
    padding-top: var(--gn-space-3);
    border-top: 1px solid var(--gn-border);
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-sm);
}

/* The editor's two disclosure blocks, side by side. Bordered so each reads as one unit
   and it is obvious which boxes belong to which claim. */
.gn-c3-edit {
    padding: var(--gn-space-3);
    border: 1px solid var(--gn-border);
    border-radius: var(--gn-radius);
}

/* The colophon: the shared rating for whatever an article is about, between the body and
   the comments. Headed, because a page carrying two ratings needs to say which is which. */
.gn-c3-colophon {
    display: flex;
    align-items: center;
    gap: var(--gn-space-3);
    flex-wrap: wrap;
    /* margin-BLOCK, not the shorthand: the shorthand's inline `0` sits below .gn-measured
       in this file and silently beat its `margin-inline: auto`, so the colophon was the one
       block on the article still hanging off the left gutter (row S4.2). */
    margin-block: var(--gn-space-4);
    padding-top: var(--gn-space-3);
    border-top: 1px solid var(--gn-border);
}

.gn-c3-colophon-label {
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-sm);
    letter-spacing: .04em;
    text-transform: uppercase;
}

/* Index cards: smaller still, and nudged off the comment count it follows. */
.gn-c3-bare {
    margin-left: var(--gn-space-2);
    gap: var(--gn-space-1);
    vertical-align: middle;
}

/* 1rem: the card's date line box is ~17px, and anything taller makes a labelled card a
   pixel or two deeper than an unlabelled one, all the way down the index. Measured. */
.gn-c3-bare .gn-c3-ring {
    flex: none;
    width: 1rem;
}

/* ── Leaderboard — ADR-052 (row S6.2) ────────────────────────────────────────
   /blog/leaderboard. The last reader-facing Bootstrap page; nav-tabs, btn-primary,
   btn-outline-secondary, table/table-responsive, alert, badge, col-md-8 and lead all
   left with this block, and ADR-043's seam test now passes on it — delete
   bootstrap.min.css and the layout suffers, the identity does not.

   It is mostly ARRANGEMENT: the two tables are .gn-index, the tabs are .gn-tabs, the
   heads are .gn-index-head, the annotations are .gn-pill, the tiers are .gn-star and the
   dial is .gn-ring. What is genuinely new is the summary card (a sibling of the trophy
   case's, which cannot be reused because .gn-case-* is scoped to TrophyCase and the blog
   is an RCL), the filter form, and two chrome-less buttons.

   Lives here and not in a .razor.css because the module ships no CSS at all (css-tokens). */

/* The handle-less notice. It replaced `alert alert-warning`: a rule and a colour, not a
   filled panel — the page has one of these and it is information, not an alarm. */
.gn-lb-note {
    margin-block: var(--gn-space-3);
    padding: var(--gn-space-3);
    border-inline-start: 3px solid var(--gn-accent);
    background: var(--gn-surface);
    font-size: var(--gn-fs-sm);
}

.gn-lb-note p {
    margin: 0;
}

.gn-lb-empty {
    margin-block: var(--gn-space-4);
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-sm);
}

/* ── Your card (§2) ──────────────────────────────────────────────────────────
   The trophy case summary's shape — dial, counts, computed next step — rebuilt under its
   own names. Wraps rather than a grid: the dial is optional (a reader with no clears gets
   the body alone) and a grid would leave its column behind. */
.gn-lb-card {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gn-space-3) var(--gn-space-4);
    margin-block: var(--gn-space-4);
    padding: var(--gn-space-3);
    border: 1px solid var(--gn-border);
    background: var(--gn-surface);
}

/* Ring and count stacked in one cell: the number sits INSIDE the arc. */
.gn-lb-dial {
    display: grid;
    place-items: center;
    margin: 0;
    flex: none;
}

.gn-lb-dial > * {
    grid-area: 1 / 1;
}

/* The ring is ProgressRing's <svg> and carries ProgressRing's own scope, so this has to be
   here rather than beside the caller — the same reason .gn-case-dial .gn-ring is in this
   file. A graphic, not a meter beside text, so it is sized off the spacing scale and no
   type step decides how big it is. */
.gn-lb-dial .gn-ring {
    inline-size: var(--gn-space-5);
    block-size: var(--gn-space-5);
    vertical-align: baseline;
}

.gn-lb-dial-n {
    font-size: var(--gn-fs-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.gn-lb-card-body {
    flex: 1 1 22rem;
    min-inline-size: 0;   /* a flex child defaults to min-content and refuses to wrap */
}

.gn-lb-line {
    margin: 0;
    font-size: var(--gn-fs-lg);
}

/* The three counts. A <dl> because that is what they are: three labelled values. */
.gn-lb-stat {
    display: flex;
    flex-wrap: wrap;
    gap: var(--gn-space-4);
    margin-block: var(--gn-space-3);
}

.gn-lb-stat div {
    margin: 0;
}

.gn-lb-stat dt {
    font-size: var(--gn-fs-2xs);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

.gn-lb-stat dd {
    margin: 0;
    font-size: var(--gn-fs-xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.gn-lb-next {
    display: flex;
    gap: var(--gn-space-2);
    margin: 0;
    color: var(--gn-text-muted);
    font-size: var(--gn-fs-sm);
}

.gn-lb-next strong {
    color: var(--gn-text);
}

.gn-lb-next-n {
    margin-inline-start: var(--gn-space-2);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ── Standings and the record ────────────────────────────────────────────────
   .gn-index-head is authored for h2; the per-post heads are h3 because they sit under
   "The record", and a level is not a size. */
.gn-lb-section {
    margin-block-end: var(--gn-space-4);
}

.gn-lb-board {
    margin-block-end: var(--gn-space-4);
}

.gn-index-head h3 {
    margin: 0;
    font-size: var(--gn-fs-xs);
    font-weight: 700;
    letter-spacing: .16em;
    text-transform: uppercase;
}

/* §8's "showing the top N of M", and the one-handle line under Standings. Both are the
   page telling the truth about what it is not showing, in the same quiet register. */
.gn-lb-cap {
    margin-block: var(--gn-space-2) 0;
    font-size: var(--gn-fs-2xs);
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

/* You, on a board of handles. --gn-accent-2 is exactly what css-tokens reserves it for:
   one item on the page, to pull an eye. The rail is the row's existing 3px card border
   recoloured; at md the card border is gone, so it is redrawn as an inset shadow, which
   also keeps it out of the zebra/hover fight over `background`. */
.gn-index tbody tr.gn-lb-you {
    border-inline-start-color: var(--gn-accent-2);
}

.gn-pill-you {
    margin-inline-start: var(--gn-space-1);
    background: var(--gn-accent-2);
    color: var(--gn-on-accent-2);
}

/* The annotations — persona, vulgarity, jargon, fingerprint. Quiet pills on purpose: one
   per row times every row is not the sparing use --gn-accent-2 is for, and these are
   context rather than status. */
.gn-lb-annot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gn-space-1);
}

.gn-lb-fp {
    font-size: var(--gn-fs-2xs);
    color: var(--gn-text-muted);
    white-space: nowrap;
}

.gn-lb-stars {
    white-space: nowrap;
    letter-spacing: .1em;
}

/* Rank-by. Inside .gn-index-head-meta, so it inherits the micro-cap register and only has
   to lay the two links out and mark the live one. */
.gn-lb-sort {
    display: flex;
    align-items: center;
    gap: var(--gn-touch-gap);
}

.gn-lb-sort a {
    padding: 2px var(--gn-space-2);
    border: 1px solid var(--gn-border-strong);
    color: var(--gn-text-muted);
    text-decoration: none;
}

.gn-lb-sort a:hover,
.gn-lb-sort a:focus-visible {
    color: var(--gn-text);
}

.gn-lb-sort a[aria-current] {
    background: var(--gn-accent);
    border-color: var(--gn-accent);
    color: var(--gn-on-accent);
}

/* ── The filter form (§5) ────────────────────────────────────────────────────
   A GET form and nothing else — no circuit, no script. It wraps rather than scrolling,
   like the tab strip: six controls on a phone is three lines, and three lines is not a
   defect (ADR-042 §7 — the document is the only scroller). */
.gn-lb-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    gap: var(--gn-space-2) var(--gn-space-3);
    margin-block: var(--gn-space-3) var(--gn-space-4);
    padding: var(--gn-space-3);
    border: 1px solid var(--gn-border);
    background: var(--gn-surface);
}

.gn-lb-filters label {
    display: block;
    font-size: var(--gn-fs-2xs);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gn-text-muted);
}

.gn-lb-filters select {
    min-block-size: var(--gn-touch);
    max-inline-size: 16rem;
    padding: var(--gn-space-1) var(--gn-space-2);
    border: 1px solid var(--gn-border-strong);
    background: var(--gn-bg);
    color: var(--gn-text);
    font-family: inherit;
    font-size: var(--gn-fs-sm);
}

/* css-tokens: a select under 16px makes Safari zoom the page on focus and never zoom back. */
[data-gn-input="coarse"] .gn-lb-filters select {
    font-size: var(--gn-fs-md);
}

.gn-lb-check {
    display: flex;
    align-items: center;
    gap: var(--gn-space-2);
    min-block-size: var(--gn-touch);
}

.gn-lb-check span {
    font-size: var(--gn-fs-sm);
    letter-spacing: 0;
    text-transform: none;
    color: var(--gn-text);
}

/* Filled, never outlined (css-tokens, owner rule) — including the quiet one, which is a
   surface fill rather than a transparent box with a border. */
.gn-lb-btn {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--gn-touch);
    padding-inline: var(--gn-space-3);
    border: 1px solid var(--gn-accent);
    background: var(--gn-accent);
    color: var(--gn-on-accent);
    font: inherit;
    font-size: var(--gn-fs-2xs);
    font-weight: 700;
    letter-spacing: .16em;
    text-transform: uppercase;
    text-decoration: none;
    cursor: pointer;
}

.gn-lb-btn:hover,
.gn-lb-btn:focus-visible {
    background: var(--gn-accent-hover);
    border-color: var(--gn-accent-hover);
    color: var(--gn-on-accent);
}

.gn-lb-btn-quiet {
    background: var(--gn-surface-2);
    border-color: var(--gn-border-strong);
    color: var(--gn-text);
}

.gn-lb-btn-quiet:hover,
.gn-lb-btn-quiet:focus-visible {
    background: var(--gn-border);
    border-color: var(--gn-border-strong);
    color: var(--gn-text);
}

/* The permutation deep-link, one per row. Small and quiet, and still a 44px target on a
   coarse pointer — the ink stays small and the target grows (css-tokens). */
.gn-lb-play {
    padding: 2px var(--gn-space-2);
    border: 1px solid var(--gn-border-strong);
    background: transparent;
    color: var(--gn-text-muted);
    font: inherit;
    font-size: var(--gn-fs-2xs);
    letter-spacing: .12em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
}

.gn-lb-play:hover,
.gn-lb-play:focus-visible {
    background: var(--gn-accent);
    border-color: var(--gn-accent);
    color: var(--gn-on-accent);
}

[data-gn-input="coarse"] .gn-lb-play {
    min-block-size: var(--gn-touch);
}

@media (min-width: 768px) {
    /* The card border is dropped at md (see the .gn-index block), so the "you" rail is
       redrawn as an inset shadow — a property no zebra or hover rule competes for. */
    .gn-index tbody tr.gn-lb-you {
        box-shadow: inset 3px 0 0 var(--gn-accent-2);
    }
}
