/* ==========================================================================
   Guide kit — subject-neutral shared styles for NEW interactive guide pages.
   Derived from assets/css/llm-guide.css, renamed to a neutral `.g-` prefix
   (llm-guide.css is the same kit under the `llmt-` prefix, still loaded by
   the 16 existing LLM-training pages; only new pages should reach for
   guide.css).

   Consumes the token layer in assets/css/theme.css and nothing else: every
   colour is `var(--color-*)`, every space is a `--space-*` step, every
   radius is `--radius-md` / `--radius-lg` / `--radius-xl` / `--radius-pill`,
   every type size is a `--text-*` step and every duration is `--dur-fast`.
   No raw hex, no bare rem for a space, no bare millisecond. Two literals
   survive and both are marked below.

   WHAT CHANGED FROM "BROADSHEET"
   1. Cool neutrals and one azure accent, from the token layer. The warm
      cream page, the warm greys and the crimson second hue are gone.
   2. The liquid glass is gone. There is no `backdrop-filter` left in this
      file. Every surface that used to blur what was behind it is now a flat
      plate: a `surface-2` fill, a hairline, and — only where the surface is
      RAISED — a shadow and a lit top edge.
   3. Recessed and raised are separated, and the separation is a rule rather
      than a taste. A `surface-2` fill is dimmer than the surface it sits in,
      so it is a WELL: hairline, no elevation, read by its fill. A `surface`
      or `surface-raised` fill is above the canvas, so it is read by its
      shadow. Notes and readouts take no elevation at all; a demo panel and
      a card do.
   4. Verdict colours are no longer series colours. `.hi` was a blue
      "correct answer" and `.bad` was the crimson accent, which had nothing
      to do with either concept. They are now `--color-ok-ink` and
      `--color-bad-ink`, which is what they always meant.
   5. No accent-coloured left rules on the callouts. `.g-goal` and `.g-warn`
      separate by INK — azure-ink lead-in versus warn-ink lead-in, under the
      icon the markup already carries — not by a coloured edge.

   NOTHING RENAMED. Every class and id in the previous version of this file
   is still here with the same spelling, because ~260 pages link it and many
   of them restyle its classes from their own inline <style> block. Only
   values changed. The one addition is `.g-prose`, at the end, and it is
   purely additive.
   ========================================================================== */


/* --------------------------------------------------------------------------
   Page frame
   --------------------------------------------------------------------------
   The 1180px is deliberately NOT `--content-width` (1160px). It is a layout
   constant this kit has always shipped and ~400 pages are composed around
   it; moving it 20px is a reflow, not a restyle. Everything below this line
   is a token. */
.g { max-width: 1180px; }
.g .page-header { padding-top: clamp(var(--space-8), 6vw, var(--space-12)); }


/* --------------------------------------------------------------------------
   The step band
   --------------------------------------------------------------------------
   A step is an article section, so its heading takes `--text-xl` — the token
   the scale documents as "section heading" — rather than a fluid clamp
   between two pixel values that no longer exist in the system. */
.g-step { padding: var(--space-12) 0; border-top: 1px solid var(--color-divider); }
.g-step:first-of-type { border-top: none; }

/* The step number is a BADGE, not a control: no href, no tabindex, not
   focusable. It therefore gets no hover and no press state, and a later pass
   should read that as a decision rather than as an oversight. */
.g-step-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--space-8); height: var(--space-8);
  border-radius: 50%;
  background: var(--color-text); color: var(--color-bg);
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: var(--text-label-sm);
  flex: 0 0 auto;
}
.g-step-head { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-1); }
.g-step-head h2 {
  margin: 0;
  font-size: var(--text-xl);
  line-height: var(--text-xl-lh);
  letter-spacing: var(--text-xl-ls);
}
.g-eyebrow {
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  color: var(--c-accent-ink);
  /* Indented to clear the step number it hangs beside. Written as the sum
     rather than the old hard-coded 42px so the indent follows the badge and
     the head's gap if either is ever re-stepped. */
  margin: 0 0 var(--space-1) calc(var(--space-8) + var(--space-3));
}


/* --------------------------------------------------------------------------
   Callouts — `.g-goal` and `.g-warn`
   --------------------------------------------------------------------------
   RECESSED. A `surface-2` fill with a hairline and no elevation at all: a
   note that casts a shadow would assert that it is above the page, which is
   the opposite of what a dimmer fill already says. Read by the fill.

   The two variants do NOT differ by a coloured left rule — that accent edge
   is exactly what the instrument theme removes. They differ by INK: the
   lead-in `<b>` is accent-ink in one and warn-ink in the other, next to the
   emoji the markup already carries, over an identical plate. */
.g-goal,
.g-warn {
  display: flex; gap: var(--space-3);
  margin: var(--space-4) 0 var(--space-5);
  padding: var(--space-4) var(--space-inset);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  font-size: var(--text-sm);
  line-height: var(--text-sm-lh);
  /* The reading measure, declared HERE, on the box that carries it. `ch`
     resolves against this element's own computed font size, so a measure
     set on an ancestor and a size set here would be two different widths. */
  max-width: var(--pref-measure);
  hyphens: auto;
}
.g-goal b { color: var(--c-accent-ink); }
.g-warn b { color: var(--color-warn-ink); }


/* --------------------------------------------------------------------------
   Math
   --------------------------------------------------------------------------
   Also a recessed well, for the same reason as the callouts: a display
   equation is read content, not floating UI, and a borderless white plate
   on the canvas is a fill difference doing a border's job. */
.g-math {
  font-family: var(--font-mono); font-size: var(--text-sm);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  padding: var(--space-3) var(--space-inset);
  border-radius: var(--radius-md);
  overflow-x: auto;
  margin: 0 0 var(--space-4);
  line-height: 1.7;
}
.g-math.g-katex { font-family: var(--font-body); font-size: var(--text-base); overflow-x: auto; }

/* Keep wide display math and tables from forcing page-level horizontal scroll on
   narrow screens: each scrolls inside its own box instead. `.katex-display` catches
   bare `$$…$$` that is not inside a `.g-math`; `.g-table-wrap` is the opt-in wrapper
   for a wide `g-cheat` table. `overscroll-behavior-x: contain` is the same split the
   slider uses on its own axis: a sideways drag that runs out of table should not
   chain-scroll the page out from under the reader. */
.katex-display { max-width: 100%; overflow-x: auto; overflow-y: hidden; }
.katex { max-width: 100%; }
.g-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; }


/* --------------------------------------------------------------------------
   The demo panel
   --------------------------------------------------------------------------
   The densest surface on a guide page and the one the tool chrome hangs off.
   RAISED, unlike the callouts: it is an object the reader manipulates, and
   its `surface-2` fill is lifted by a real elevation pair rather than by a
   blur. `--elev-shadow` and `--elev-inset` are declared as locals and
   combined in ONE `box-shadow`, so a later state change can move the
   elevation without silently dropping the lit top edge. */
.g-demo {
  --elev-shadow: var(--c-elev-2);
  --elev-inset: inset 0 1px 0 var(--c-elev-inset-hi);
  display: flex; gap: var(--space-5); flex-wrap: wrap; align-items: flex-start;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  box-shadow: var(--elev-shadow), var(--elev-inset);
  padding: var(--space-inset);
  margin-top: var(--space-2);
}
.g-demo > *, .g-grid2 > *, .g-grid3 > * { min-width: 0; }

.g-canvas-wrap { flex: 1 1 380px; min-width: 280px; max-width: 100%; }
/* The plot is a WELL and the well is the canvas colour — one step below the
   panel it sits in, in both themes, so the plot is a real recess without
   costing the palette a dedicated token. The hairline is an INSET box-shadow
   rather than a border on purpose: `guide-core.js` sizes the backing store
   from this element's box, and a border would move that box by 2px for a
   reason that has nothing to do with the data. */
.g-canvas-wrap canvas {
  width: 100%; height: auto; display: block;
  background: var(--plot-bg);
  box-shadow: inset 0 0 0 1px var(--color-border);
  border-radius: var(--radius-md);
}
.g-canvas-hint { font-size: var(--text-micro); color: var(--color-text-subtle); margin-top: var(--space-1); }


/* --------------------------------------------------------------------------
   The control column
   --------------------------------------------------------------------------
   DENSITY, not decoration. A control column holds six fields, so its own
   spacing is the densest rhythm in the kit and reads a step tighter than the
   page around it. */
.g-controls { flex: 1 1 260px; min-width: 240px; max-width: 100%; font-size: var(--text-sm); }
.g-field { margin-bottom: var(--space-4); }
.g-field label {
  display: flex; justify-content: space-between; gap: var(--space-2);
  margin-bottom: var(--space-1);
  font-size: var(--text-label-sm);
  line-height: var(--text-label-sm-lh);
  color: var(--color-text-muted);
}
/* The value a slider has printed back. Tabular figures because it changes
   under the thumb and proportional digits make the last digit jump sideways
   as the number grows. */
.g-field label span.val {
  font-family: var(--font-mono);
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

/* ---- range inputs --------------------------------------------------------
   THE HIT-AREA QUARTET. `box-sizing: content-box`, `height: 4px`,
   `padding-block` and `margin-block` and `background-clip: content-box` are
   one mechanism, and dropping ANY ONE of them makes the track vanish:

     · the global `* { box-sizing: border-box }` in styles.css would make
       `height: 4px` the BORDER box, so the 20px of padding would eat the
       content box entirely and `background-clip: content-box` would paint
       the gradient into nothing;
     · the padding is what makes a 4px-tall control a 24px hit target;
     · the negative block margin takes exactly that 20px back out of the
       flow, so `4 + 20 - 20 = 4` and nothing below the slider moves;
     · the clip is what stops the padding from being SEEN — without it the
       4px track is painted as a 24px bar.

   Do not tidy this up. It is load-bearing and it is measured, not guessed.

   Both `.g-field input` and `.g-controls input` are targeted: most of the
   kit's sliders sit inside a `.g-field`, but a large minority are direct
   children of the control column with only a `.val` label, and both shapes
   have to get the same treatment. */
.g-field input[type=range],
.g-controls input[type=range] {
  /* The filled portion. Nothing in this repo writes `--pct` — `bindSliders()`
     in guide-core.js mirrors the value into the adjacent `.val` span and
     stops there — so it is declared at 0% and the track renders as an
     unfilled groove in `--c-border-strong`. Declaring the hook costs
     nothing and means a page that starts writing it gets the filled track
     for free, with no change here. The alternative — leaving the input's
     native appearance alone — is not available, because a native range
     squeezed to a 4px box squashes the thumb to 4px with it.

     The two colours are written as the `--c-*` semantic names rather than
     the `--color-*` aliases, and that is not a style preference. ONE dead
     `var()` inside a gradient invalidates the WHOLE `background-image` at
     computed-value time, not just its own stop — so an alias that drifts out
     of theme.css does not degrade the track, it deletes it. `--c-accent` and
     `--c-border-strong` are the names the `--color-*` block is itself defined
     in terms of, so they are the stable end of the indirection. (This file
     was written against a theme.css that also exported `--color-accent-ink`
     and `--color-border-strong`; both aliases have since been dropped, so
     `--c-accent-ink` is used directly for the same reason.) */
  --pct: 0%;
  width: 100%;
  height: 4px;
  box-sizing: content-box;
  padding-block: 10px;
  margin-block: -10px;
  background-clip: content-box;
  -webkit-background-clip: content-box;
  border-radius: var(--radius-1);
  background-image: linear-gradient(
    to right,
    var(--color-accent) 0 var(--pct),
    var(--c-border-strong) var(--pct) 100%
  );
  -webkit-appearance: none;
  appearance: none;
  /* `pan-y`, not `none`. A vertical swipe that starts on a 4px-tall control
     must still scroll the page, or every miss on a phone is a dead end.
     A horizontal drag is the slider's. */
  touch-action: pan-y;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.g-field input[type=range]::-webkit-slider-thumb,
.g-controls input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--color-text);
  border: 1px solid var(--c-border-strong);
  /* A 3px ring in the PANEL's own fill, so the thumb reads as sitting on the
     groove rather than floating over it. */
  box-shadow: 0 0 0 3px var(--color-surface-2);
  cursor: pointer;
  transition: transform var(--dur-fast) ease;
}
.g-field input[type=range]::-moz-range-thumb,
.g-controls input[type=range]::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--color-text);
  border: 1px solid var(--c-border-strong);
  box-shadow: 0 0 0 3px var(--color-surface-2);
  cursor: pointer;
  transition: transform var(--dur-fast) ease;
}

/* Hover is an ENHANCEMENT, so it sits behind a pointer query: on touch the
   hover state sticks after the tap and the slider never returns to rest.
   `:active` is deliberately OUTSIDE the query, because unlike `:hover` it
   does fire on touch — on a range input it means "the drag is live", which
   is information a finger cannot get any other way. `scale` rather than a
   translate: growing the thumb about its own centre cannot move it out from
   under the pointer, and 1.18 rather than 1.12 because a press is a
   confirmation, not an invitation. */
@media (hover: hover) and (pointer: fine) {
  .g-field input[type=range]:hover::-webkit-slider-thumb,
  .g-controls input[type=range]:hover::-webkit-slider-thumb { transform: scale(1.12); }
  .g-field input[type=range]:hover::-moz-range-thumb,
  .g-controls input[type=range]:hover::-moz-range-thumb { transform: scale(1.12); }
}
.g-field input[type=range]:active::-webkit-slider-thumb,
.g-controls input[type=range]:active::-webkit-slider-thumb { transform: scale(1.18); }
.g-field input[type=range]:active::-moz-range-thumb,
.g-controls input[type=range]:active::-moz-range-thumb { transform: scale(1.18); }

/* A range takes a ring on the THUMB, not an outline: a 2px outline around a
   4px-tall track is all but invisible, and this is the same 40% accent wash
   every other focusable thing on the site uses, so "focus is here" is one
   visual event across the whole tool. */
.g-field input[type=range]:focus-visible,
.g-controls input[type=range]:focus-visible { outline: none; }
.g-field input[type=range]:focus-visible::-webkit-slider-thumb,
.g-controls input[type=range]:focus-visible::-webkit-slider-thumb {
  box-shadow:
    0 0 0 3px var(--color-surface-2),
    0 0 0 6px color-mix(in srgb, var(--color-accent) 40%, transparent);
}
.g-field input[type=range]:focus-visible::-moz-range-thumb,
.g-controls input[type=range]:focus-visible::-moz-range-thumb {
  box-shadow:
    0 0 0 3px var(--color-surface-2),
    0 0 0 6px color-mix(in srgb, var(--color-accent) 40%, transparent);
}

/* Typed input, text areas, selects. RAISED — a `surface` fill on the canvas
   reads as above it, so these take a hairline and, on focus, a wash rather
   than a shadow. */
.g-field input[type=text],
.g-field textarea,
.g-field select {
  width: 100%;
  font-family: var(--font-mono); font-size: var(--text-sm);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface); color: var(--color-text);
  transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.g-field textarea { resize: vertical; min-height: var(--space-16); }

/* 18% is the accent ladder's "active" step, reused here rather than the
   slider's 40%: that one is a tight ring around a 16px disc, and this is a
   wide wash around a whole field, where the same alpha would be a wall. */
.g-field input[type=text]:focus-visible,
.g-field textarea:focus-visible,
.g-field select:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 18%, transparent);
}


/* --------------------------------------------------------------------------
   Buttons
   --------------------------------------------------------------------------
   `.g-btnrow button` IS a control — it is a real <button>, focusable and
   operable — so unlike the step badge it does get hover and press states,
   both gated on a real pointer. `color` is in the transition list because the
   selected and primary states change the label as well as the fill, and
   without it the text swapped instantly while the fill eased, which reads as
   two different presses. */
.g-btnrow { display: flex; gap: var(--space-2); flex-wrap: wrap; margin: var(--space-3) 0 var(--space-4); }
.g-btnrow button {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: var(--text-label-sm); line-height: var(--text-label-sm-lh);
  cursor: pointer;
  border: 1px solid var(--color-border);
  background: var(--color-surface); color: var(--color-text);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  -webkit-tap-highlight-color: transparent;
  transition:
    background-color var(--dur-fast) ease,
    border-color var(--dur-fast) ease,
    color var(--dur-fast) ease,
    opacity var(--dur-fast) ease;
}

/* Primary is INVERTED, not accent-filled. The accent carries interaction —
   links, focus, the active row — and is never a button fill. A guide page
   can carry twenty of these; they have to recede until the reader goes
   looking for one. */
.g-btnrow button.g-primary {
  background: var(--color-text); color: var(--color-bg);
  border-color: var(--color-text);
}

/* Selected: the ladder's 10% "active row" wash and a real border — the same
   vocabulary as a selected nav row and a revealed quiz answer. */
.g-btnrow button.active {
  background: color-mix(in srgb, var(--color-accent) 10%, var(--color-surface));
  border-color: var(--color-accent);
  color: var(--c-accent-ink);
}

/* The `:not(.active)` guard is not redundant. `.g-btnrow button:hover:not(…)`
   is more specific than `.g-btnrow button.active`, so without it hovering a
   selected button would erase its own selection. */
@media (hover: hover) and (pointer: fine) {
  .g-btnrow button:hover:not(.g-primary):not(.active) {
    background: var(--color-surface-2);
    border-color: var(--c-border-strong);
  }
  /* A selected row that is also under the pointer goes one step stronger —
     18%, the ladder's "active-while-active". */
  .g-btnrow button.active:hover:not(.g-primary) {
    background: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface));
  }
  .g-btnrow button.g-primary:hover { opacity: 0.85; }
  .g-toc a:hover { color: var(--c-accent-ink); text-decoration: underline; text-underline-offset: 0.18em; }
  .g-quiz-choice:hover:not(:disabled):not(.correct):not(.incorrect) { background: var(--color-surface-2); }
}

/* `:not(:disabled)` is load-bearing in a way that looks redundant: a disabled
   button still matches `:active` in both engines while it is being pointed
   at, so without the guard a dead control would brighten at the exact moment
   the reader tried to use it. */
.g-btnrow button.g-primary:active:not(:disabled) { opacity: 0.7; }
.g-btnrow button:active:not(:disabled):not(.g-primary) { border-color: var(--c-border-strong); }
.g-btnrow button:disabled,
.g-btnrow button.g-primary:disabled:hover { opacity: 0.45; cursor: not-allowed; }


/* --------------------------------------------------------------------------
   Readout
   --------------------------------------------------------------------------
   RECESSED, like the notes: a well the numbers are read out of, so a hairline
   and no shadow. Tabular figures throughout, because a readout's whole job is
   being compared against the last one and a proportional digit moves sideways
   when it gains a place.

   The verdict colours are the point of this block. `.hi` and `.bad` used to
   be a blue and a crimson borrowed from the series palette; they are now the
   ok and bad status inks, which is what they have meant on every one of
   these pages all along. `.k` was 60% opacity, which also dimmed any child it
   wrapped; a colour does the same job without that side effect. */
.g-readout {
  font-family: var(--font-mono);
  font-size: var(--text-label-sm);
  line-height: var(--read-line);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  font-variant-numeric: tabular-nums;
}
.g-readout .k { color: var(--color-text-subtle); }
.g-readout .hi { color: var(--color-ok-ink); font-weight: 600; }
.g-readout .bad { color: var(--color-bad-ink); font-weight: 600; }


/* --------------------------------------------------------------------------
   Token strip
   --------------------------------------------------------------------------
   A token is a CHIP, so `--radius-sm` and a hairline step, not a pill: it
   sits in a row with its neighbours and a pill would out-shout the tokenizer
   the row exists to show. */
.g-tokens { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.g-tok {
  font-family: var(--font-mono); font-size: var(--text-label-sm);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--color-accent-100); color: var(--color-accent-800);
  border: 1px solid var(--color-accent-200);
  position: relative;
}
/* LITERAL 9.5px, and the only type size in this file that is not a token.
   The scale's smallest step is `--text-micro` at 11px, and 11px would make
   the id line as tall as the token it labels, doubling the height of every
   chip in a 50-token row. Inventing a step below `--text-micro` would put a
   one-off value into a scale the reader's text-size preference has to reach,
   which is the one thing that scale must not contain. */
.g-tok .id {
  display: block;
  font-size: 9.5px;
  /* The old 0.6 opacity, expressed as a colour instead. Mixing toward the
     chip's OWN fill rather than toward the page is what makes this read the
     same in both themes, where the 800 and 100 steps swap places. */
  color: color-mix(in srgb, var(--color-accent-800) 62%, var(--color-accent-100));
  margin-top: var(--space-hair);
}


/* --------------------------------------------------------------------------
   Cheat sheet
   --------------------------------------------------------------------------
   `th` drops its 0.6 opacity for a real muted colour, and its rule darkens to
   `--c-border-strong` so the header row is separated from the body by
   more than the body rows are from each other. */
.g-cheat { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.g-cheat th,
.g-cheat td {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-divider);
  vertical-align: top;
  text-align: start;
}
.g-cheat th {
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--color-text-subtle);
  border-bottom-color: var(--c-border-strong);
}
.g-cheat code { white-space: nowrap; }

.g-refs { font-size: var(--text-sm); line-height: calc(var(--read-line) * 1.0857); }
.g-refs li { margin-bottom: var(--space-hair); }


/* --------------------------------------------------------------------------
   Table of contents
   --------------------------------------------------------------------------
   Was a blur panel. Now a recessed well: a jump list is a reading aid
   sitting in the page, not an object floating over it, so it takes the same
   fill-and-hairline treatment as the callouts and no elevation. */
.g-toc {
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4);
  font-size: var(--text-label-sm);
  margin: var(--space-4) 0 0;
  padding: var(--space-3) var(--space-inset);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}
.g-toc a { color: var(--color-text-muted); text-decoration: none; }


/* --------------------------------------------------------------------------
   Grids and cards
   --------------------------------------------------------------------------
   `.g-card` is RAISED: a `surface-2` fill lifted by an elevation pair. It is
   the one kit surface that hovers, and the hover moves `--elev-shadow` alone
   so the lit top edge survives the change. */
.g-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
.g-grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); }
.g-card {
  --elev-shadow: var(--c-elev-1);
  --elev-inset: inset 0 1px 0 var(--c-elev-inset-hi);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-shadow), var(--elev-inset);
  padding: var(--space-4) var(--space-inset);
}
@media (hover: hover) and (pointer: fine) {
  .g-card:hover { --elev-shadow: var(--c-elev-2); }
}


/* --------------------------------------------------------------------------
   Colour key
   --------------------------------------------------------------------------
   Sized off `--chart-legend-size`, which is the token the chart layer already
   reserves for exactly this row of series names next to a plot.

   A swatch is a swatch: no href, no tabindex, not focusable, so no hover and
   no press state. It is labelled by the text beside it. */
.g-legend { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); font-size: var(--chart-legend-size); margin-top: var(--space-2); }
.g-legend-item { display: inline-flex; align-items: center; gap: var(--space-2); }
.g-swatch { display: inline-block; width: 11px; height: 11px; border-radius: var(--radius-1); flex: 0 0 auto; }
.g-source { font-size: var(--text-micro); color: var(--color-text-subtle); margin-top: var(--space-1); }


/* --------------------------------------------------------------------------
   Image strips and spectrograms — a row of labelled pixel/spectrogram panels.
   `.g-imgcell` is the figure, its canvas the plate; `.g-spec` is the mono
   caption under a spectrogram or a numeric spec line.
   --------------------------------------------------------------------------
   Same well as `.g-canvas-wrap canvas`, and the same inset hairline for the
   same reason: the canvas routines size the backing store from this box. */
.g-imgrow { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: flex-end; }
.g-imgcell { flex: 1 1 120px; min-width: 96px; max-width: 220px; }
.g-imgcell canvas {
  width: 100%; height: auto; display: block;
  background: var(--plot-bg);
  box-shadow: inset 0 0 0 1px var(--color-border);
  border-radius: var(--radius-md);
}
.g-imgcell figcaption {
  font-size: var(--text-micro);
  color: var(--color-text-muted);
  margin-top: var(--space-1);
  text-align: center;
  font-family: var(--font-mono);
}
.g-spec {
  font-family: var(--font-mono); font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
}


/* --------------------------------------------------------------------------
   Live/typeset matrices — generic bracketed CSS grid, updated in place by
   LinAlg.setMatrix. Static equations should use KaTeX (.g-math.g-katex) instead;
   re-running renderMathInElement every frame is a jank source.

   The bracket glyphs stay a raw 2.6em: they are a typographic ornament
   scaling with the surrounding matrix type, not a space or a text step, and
   there is no token for "as tall as the thing it brackets". */
.g-matrix { display: inline-flex; align-items: center; gap: var(--space-2); vertical-align: middle; }
.g-matrix-wrap { display: inline-flex; align-items: stretch; vertical-align: middle; }
.g-bracket {
  font-family: var(--font-mono); font-size: 2.6em; font-weight: 300; line-height: 1;
  display: flex; align-items: center; transform: scaleX(0.6);
  user-select: none; opacity: 0.85;
}
.g-matrix-grid {
  display: inline-grid; grid-template-columns: repeat(var(--g-matrix-cols, 3), minmax(1.7em, auto));
  align-items: center; justify-items: end;
  gap: var(--space-1) var(--space-4);
  padding: var(--space-hair) var(--space-2);
}
.g-matrix-cell { font-family: var(--font-mono); font-size: var(--text-sm); white-space: nowrap; font-variant-numeric: tabular-nums; }
/* `--hi` on a matrix cell means "this is the entry under discussion" — a
   selection, not a verdict — so it takes the accent ink, not the ok ink. */
.g-matrix-cell--hi { color: var(--c-accent-ink); font-weight: 600; }
.g-matrix-cell.g-matrix-label { font-family: var(--font-body); font-size: var(--text-micro); color: var(--color-text-subtle); justify-self: start; }
.g-matrix-label-out { font-family: var(--font-body); font-size: var(--text-sm); }
.g-matrix-input-cell {
  width: 4.6em; font-family: var(--font-mono); font-size: var(--text-label-sm);
  text-align: right;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-divider); border-radius: var(--radius-sm);
  background: var(--color-surface); color: var(--color-text);
}
.g-matrix-input-cell:focus-visible {
  border-color: var(--color-accent); outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 18%, transparent);
}


/* --------------------------------------------------------------------------
   "check your understanding" quiz, driven by guide-quiz.js (window.GuideQuiz).
   --------------------------------------------------------------------------
   Same semantic migration as the readout, and for the same reason: a page
   that marked a wrong answer crimson and a right one blue was borrowing the
   series palette for a job the series palette has nothing to do with. The
   fills are the ladder's 10% wash on the surface, one step for each verdict.
   The `:not(.correct):not(.incorrect)` guards on the hover rules are needed
   because a bare `:hover:not(:disabled)` outranks `.g-quiz-choice.correct`
   on specificity and would grey out the answer the reader is looking at. */
.g-quiz-head { display: flex; align-items: center; justify-content: flex-end; margin: calc(-1 * var(--space-2)) 0 var(--space-1); }
/* A badge: not a control, no hover, no press state. */
.g-quiz-badge {
  font-family: var(--font-mono); font-size: var(--text-label-sm);
  padding: var(--space-hair) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--color-surface-2); color: var(--color-text-muted);
  border: 1px solid var(--color-border);
}
.g-quiz-badge.done {
  background: color-mix(in srgb, var(--color-ok) 10%, var(--color-surface));
  color: var(--color-ok-ink);
  border-color: color-mix(in srgb, var(--color-ok) 30%, var(--color-border));
}
.g-quiz-q { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--color-divider); }
.g-quiz-q:first-child { margin-top: var(--space-2); padding-top: 0; border-top: none; }
.g-quiz-prompt { margin: 0 0 var(--space-3); font-weight: 600; font-size: var(--text-base); line-height: var(--text-base-lh); }
.g-quiz-qnum { color: var(--color-text-subtle); font-weight: 400; }
.g-quiz-choice-list { display: flex; flex-direction: column; gap: var(--space-2); }
.g-quiz-choice {
  text-align: start;
  font-family: var(--font-body); font-size: var(--text-sm); line-height: var(--text-sm-lh);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md); cursor: pointer;
  border: 1px solid var(--color-border);
  background: var(--color-surface); color: var(--color-text);
  transition: background-color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.g-quiz-choice:hover:not(:disabled) { border-color: var(--c-border-strong); }
.g-quiz-choice.correct { border-color: var(--color-ok); background: color-mix(in srgb, var(--color-ok) 10%, var(--color-surface)); }
.g-quiz-choice.incorrect { border-color: var(--color-bad); background: color-mix(in srgb, var(--color-bad) 10%, var(--color-surface)); }
/* The old 0.9 opacity is gone on purpose. A disabled option here is not dead
   — it is the REVEALED answer, and dimming it fights the one colour signal
   the question exists to deliver. */
.g-quiz-choice:disabled { cursor: default; }
.g-quiz-choice:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.g-quiz-numeric-row { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }
.g-quiz-numeric-row input[type=text] {
  flex: 1 1 160px; min-width: 120px;
  font-family: var(--font-mono); font-size: var(--text-sm);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md); border: 1px solid var(--color-border);
  background: var(--color-surface); color: var(--color-text);
}
.g-quiz-numeric-row input[type=text]:focus-visible {
  border-color: var(--color-accent); outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 18%, transparent);
}
.g-quiz-numeric-row input[type=text]:disabled { opacity: 0.7; }
.g-quiz-numeric-row .g-btnrow { margin: 0; }
.g-quiz-feedback { font-size: var(--text-label-sm); font-weight: 600; margin-top: var(--space-2); }
.g-quiz-feedback.hi { color: var(--color-ok-ink); }
.g-quiz-feedback.bad { color: var(--color-bad-ink); }
/* A mini reading column, and the one existing surface in this kit that takes
   the full prose treatment: it is the only run of sentences a quiz page
   emits. `--text-prose` is set HERE, on the element carrying the measure —
   `ch` resolves against this element's own font size, so a 70ch measure with
   a 13px body and the same 70ch measure with a 17px body are two different
   widths, and only one of them is seventy characters. */
.g-quiz-explain {
  font-size: var(--text-prose);
  line-height: var(--read-line);
  max-width: var(--pref-measure);
  margin-top: var(--space-1);
  color: var(--color-text);
  hyphens: auto;
}
.g-quiz-empty { font-size: var(--text-sm); color: var(--color-text-subtle); font-style: italic; }


/* --------------------------------------------------------------------------
   The reading column
   --------------------------------------------------------------------------
   Additive, and the only selector this migration adds. Everything above is
   chrome and widgets; this is the article itself, for pages that want one.

   `--text-prose` goes on the COLUMN, never on a descendant. `ch` is resolved
   against the element's own computed font size, so a column that declares
   the measure and a body that declares the size are measuring two different
   types unless the column owns both — and the difference is invisible until
   the reader raises the text size, at which point the measure is quietly
   wrong. */
.g-prose {
  font-size: var(--text-prose);
  line-height: var(--read-line);
  max-width: var(--pref-measure);
  color: var(--color-text);
  hyphens: auto;
  /* Safari only; the other engines parse it and do nothing. Kept because it
     is one declaration, it is right in the engine that honours it, and it
     applies to the first and last characters of a line only — there is no
     flex or grid inside prose, so it cannot interact with block flow. */
  hanging-punctuation: first last;
}
/* Block rhythm in `em` of the prose size, scaled by the reader's leading, so
   the gap between two paragraphs grows with the type and breathes with
   --read-line instead of staying a number tuned once. */
.g-prose > * + * { margin-top: calc(1.0824em * var(--read-lead)); }
.g-prose ul, .g-prose ol { padding-left: var(--space-6); }
.g-prose li + li { margin-top: var(--space-tight); }
/* Links in long-form prose carry a persistent underline, so they are
   distinguishable without relying on colour alone. 45% is the ladder's prose
   underline step. */
.g-prose a {
  color: var(--c-accent-ink);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--color-accent) 45%, transparent);
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
}
.g-prose code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  background: var(--color-code-bg);
  padding: var(--space-hair) var(--space-1);
  border-radius: var(--radius-1);
}
.g-prose blockquote { color: var(--color-text); font-weight: 500; line-height: calc(var(--read-line) * 0.92); }


/* --------------------------------------------------------------------------
   Selection
   --------------------------------------------------------------------------
   22% is the ladder's selection step. The explicit `color` is load-bearing
   rather than decorative: `::selection` is painted with the matched
   ancestor's own resolved pair, and the kit's prose is full of KaTeX spans
   that inherit — a formula selected across a line break would otherwise be
   half accent ink and half prose colour, and the highlight would read as a
   rendering fault. Pinning the foreground to the text colour makes the wash a
   background change and leaves the glyphs on it contrast-guarded. */
.g ::selection {
  background-color: color-mix(in srgb, var(--color-accent) 22%, transparent);
  color: var(--color-text);
}


/* --------------------------------------------------------------------------
   Scrollbars
   --------------------------------------------------------------------------
   Scoped to the kit's OWN scrollers rather than declared globally. A
   persistently visible scrollbar is how a reader knows an equation or a table
   continues past the measure, and 10px with a transparent track is the whole
   signal — but a shared kit is not the place to restyle every scrollbox on a
   page whose other chrome other files own. The base layer owns the global
   default; this owns the four boxes below.

   The standard properties are behind a POSITIVE Blink/WebKit check, and that
   is not a stylistic choice. Because a non-`auto` `scrollbar-width` makes the
   user agent ignore `::-webkit-scrollbar` outright (CSSOM Scrollbars: EITHER
   standard property being non-auto is enough), putting them in an ordinary
   block would silently replace the 10px transparent-track scrollbar above with
   the browser default in every Blink engine. The obvious
   `@supports not (selector(::-webkit-scrollbar))` is worse still: Chromium
   reports `false` for that query while implementing the pseudo-elements, so
   the block applied everywhere. `:-webkit-any-link` is Blink/WebKit-only, so
   asking for its ABSENCE is a positive test for the engines that need the
   standard properties. */
:where(.g-math, .g-table-wrap, .katex-display, .g-spec) {
  -webkit-overflow-scrolling: touch;
}
@supports not (selector(:-webkit-any-link)) {
  :where(.g-math, .g-table-wrap, .katex-display, .g-spec) {
    scrollbar-width: thin;
    scrollbar-color: var(--c-border-strong) transparent;
  }
}
:where(.g-math, .g-table-wrap, .katex-display, .g-spec)::-webkit-scrollbar { width: 10px; height: 10px; }
:where(.g-math, .g-table-wrap, .katex-display, .g-spec)::-webkit-scrollbar-track { background: transparent; }
:where(.g-math, .g-table-wrap, .katex-display, .g-spec)::-webkit-scrollbar-thumb { background: var(--c-border-strong); border-radius: var(--radius-sm); }
:where(.g-math, .g-table-wrap, .katex-display, .g-spec)::-webkit-scrollbar-thumb:hover { background: var(--color-text-subtle); }


/* --------------------------------------------------------------------------
   Narrow viewports
   --------------------------------------------------------------------------
   Unchanged from before: the eyebrow stops indenting past the step badge, the
   step heading wraps instead of squeezing, and the grids become one column. */
@media (max-width: 640px) {
  .g-eyebrow { margin-left: 0; }
  .g-step-head { flex-wrap: wrap; }
  .g-grid2, .g-grid3 { grid-template-columns: 1fr; }
  .g-quiz-head { justify-content: flex-start; margin-top: var(--space-2); }
}
