/* SAT Traffic Lights — aligned aesthetic (v2).
 * Visual system mirrors homepage v4-refined (the shipping aidroplets.com look):
 *   • Geist + Geist Mono
 *   • Single calibrated lavender accent (no rainbow gradients)
 *   • Dark canvas, restrained card chrome
 *   • Status colors (green/yellow/red) reserved for the confidence signal only
 *   • Tabular figures on numeric content
 *
 * HTML structure and class names are unchanged from the previous stylesheet so
 * app.js continues to work without modification. Only the look-and-feel moves.
 */

:root {
  /* Font stack — body stays Geist (highly readable sans) and titles
   * use Space Grotesk (geometric oddball, more character). Both load
   * via Google Fonts in index.html. The `--stl-font-display` var lets
   * us swap heading fonts in one place without combing through every
   * h1/h2/h3 rule. */
  --stl-font-body:    "Geist", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --stl-font-display: "Space Grotesk", "Geist", system-ui, sans-serif;
  --stl-font-mono:    "Geist Mono", ui-monospace, "SF Mono", monospace;

  /* surfaces — never pure black */
  --stl-bg-deep:      #050509;
  --stl-bg:           #0a0b12;
  --stl-bg-2:         #0d0e18;
  --stl-surface:      rgba(255, 255, 255, 0.035);
  --stl-surface-2:    rgba(255, 255, 255, 0.055);
  --stl-surface-3:    rgba(255, 255, 255, 0.075);
  --stl-hairline:     rgba(255, 255, 255, 0.07);
  --stl-hairline-hi:  rgba(255, 255, 255, 0.12);

  /* text — never pure white */
  --stl-text:         #f0f2f7;
  --stl-text-muted:   #a8b0c4;
  /* WCAG AA fix 2026-08-17. Was #6b7288 = 4.01:1 on --stl-bg-2 and 4.10:1 on
     --stl-bg, under the 4.5:1 AA floor for normal text — across all 138 uses.
     #787e92 clears it with headroom (4.76:1 worst case) and is 13 RGB points
     lighter, so the step down from --stl-text-muted is unchanged.
     Same value as /beta/'s --subtle: same token, and the two are meant to
     read as one product. Light theme needed no change (already 5.33:1).
     Pinned by tests/text-contrast.test.js. */
  --stl-text-subtle:  #787e92;
  --stl-text-faint:   #494f60;

  /* one accent per surface */
  --stl-accent:       #8b86ff;
  --stl-accent-2:     #5d8dff;
  --stl-accent-soft:  rgba(139, 134, 255, 0.12);
  --stl-accent-line:  rgba(139, 134, 255, 0.4);

  /* functional status colors (signal only) — saturated mid-tones for
   * dots, chips, borders, and gradient sweeps where saturation reads
   * cleanly against the dark canvas. */
  --stl-green:        #4ade80;
  --stl-yellow:       #facc15;
  --stl-red:          #fb7185;
  /* RGB triplets of the three above, for rgba() composition. Same
   * convention as --stl-rainbow-mid-rgb and --tile-tint: a box-shadow
   * needs a colour with alpha, and you cannot get one out of a hex
   * custom property without them. Keep in step with the hexes. */
  --stl-green-rgb:    74, 222, 128;
  --stl-yellow-rgb:   250, 204, 21;
  --stl-red-rgb:      251, 113, 133;

  /* Text-on-dark variants of the same hues — lighter pastels so big
   * numerals and stat labels stay readable. The mid-tone signal
   * colors above look fine as small marks but visibly recede when
   * blown up to 40+px text against #050509. These are roughly the
   * Tailwind 300-tier siblings of the values above. */
  --stl-green-text:   #86efac;
  --stl-yellow-text:  #fde047;
  --stl-red-text:     #fda4af;

  /* FIXLIST P1 (2026-08-18) — colours that were shipping as literals with
   * no token to reach for:
   *   amber : the "gap" outcome + admin activity warnings ("#f59e0b, a
   *           seventh signal-adjacent colour with no token" — GAPS.md)
   *   blue  : the insane tier / AI-generated hue (#60a5fa family, was
   *           hardcoded in app.css AND signal-theme.css independently)
   * Light values follow the same rule as the signal trio: saturated
   * mid-tones that pass on white. */
  --stl-amber:        #f59e0b;
  --stl-amber-rgb:    245, 158, 11;
  --stl-amber-text:   #fcd34d;
  --stl-blue:         #60a5fa;
  --stl-blue-rgb:     96, 165, 250;
  --stl-blue-text:    #93c5fd;

  /* The opaque interior + hover interior for the rainbow gradient-border
   * trick (background: linear-gradient(fill,fill) padding-box, rainbow
   * border-box). Was hardcoded #15161d/#1c1d25 at ~10 sites, each needing
   * its own light-theme patch; now the token flips once. */
  --stl-rainbow-fill:       #15161d;
  --stl-rainbow-fill-hover: #1c1d25;

  /* Frosted-glass surface for FLOATING panels — menus, dropdowns,
   * popovers. ~92% tint + a backdrop blur ON THE CONSUMER (the blur
   * cannot live in a colour token): behind-content reads as a soft glow,
   * never as legible text. The failure this replaces was the user menu
   * at 5.5% --stl-surface-2 — readable page text through the menu
   * (Joshua 2026-08-18); his follow-up: fully opaque lost the depth,
   * "I still wanted some opacity." Same recipe as the toast. */
  --stl-panel: rgba(22, 23, 33, 0.92);

  /* Input hover border. Was raw rgba(255,255,255,.18), which VANISHES in
   * light mode (lighter than the resting border on a white canvas). */
  --stl-hairline-hover: rgba(255, 255, 255, 0.18);

  /* ============ Generic "rainbow" accent (G5+teal — Stripe tri-tone + green) ============
   * Teal → sky → violet → pink, four-stop gradient. The teal at the
   * cool end transitions into sky through aqua, so the green reads
   * as part of the cool-palette family rather than a separate hue
   * jump. Used in place of a single white/lavender accent for any
   * surface that's NOT bound to a specific test type. Lives at :root
   * so any file can pull it directly. The single-color fallback
   * (--stl-rainbow-mid) is the violet near-midpoint — used in glows,
   * shadows, and other places where a CSS gradient can't apply
   * (e.g. box-shadow color, rgba alpha tints). The RGB triplet
   * (--stl-rainbow-mid-rgb) lets rgba(...) calls compose against it
   * the same way --stl-tint does for test colors.
   *
   * Previous gradient: G5 Stripe-style (no green)
   *   linear-gradient(90deg, #60a5fa 0%, #a78bfa 50%, #f0abfc 100%)
   * Older fallback: G2 cool spectrum
   *   linear-gradient(90deg, #a855f7 0%, #6366f1 33%, #3b82f6 66%, #06b6d4 100%)
   *   mid #6366f1 / 99, 102, 241
   * Both stored in the rainbow-gradients reference memory for easy
   * fallback if this doesn't land.
   *
   * The canvas-gradient version (for the Chart.js radar polygon)
   * lives in app.js → buildRainbowCanvasGradient; both must change
   * together when swapping. */
  --stl-rainbow: linear-gradient(90deg, #5eead4 0%, #60a5fa 33%, #a78bfa 66%, #f0abfc 100%);
  --stl-rainbow-soft: linear-gradient(90deg,
    rgba( 94, 234, 212, 0.16)  0%,
    rgba(var(--stl-blue-rgb), 0.16) 33%,
    rgba(167, 139, 250, 0.16) 66%,
    rgba(240, 171, 252, 0.16) 100%);
  --stl-rainbow-mid:     #a78bfa;          /* violet near-midpoint */
  --stl-rainbow-mid-rgb: 167, 139, 250;

  /* spacing — raw step scale */
  --s-1:  4px;  --s-2:  8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 20px;  --s-6: 24px;  --s-7: 32px; --s-8: 40px;

  /* Vertical-spacing INTENT tokens — use these (not raw --s-* values)
   * when separating chunks of content inside a hero card so every page
   * follows the same rhythm. Picking by intent means "all subsection
   * separators look the same across pages" without anyone having to
   * remember which step is the right one.
   *
   *   --gap-hug      : 8px  — paired title + its immediate subtitle
   *   --gap-tight    : 12px — items within a single group (chip row,
   *                            ledger row internals)
   *   --gap-normal   : 16px — default subsection separator; the gap
   *                            between distinct chunks inside one card
   *   --gap-section  : 20px — major section break inside the same card
   *                            (e.g. head → main, main → CTA)
   *   --gap-hero     : 32px — between a card's hero head and its first
   *                            real interactive section (used sparingly)
   *
   * Rule of thumb when deciding which to use:
   *   "Are these two siblings the same kind of thing?" → --gap-tight
   *   "Same area of the page, just different parts?"   → --gap-normal
   *   "These are distinctly separate areas?"           → --gap-section
   */
  --gap-hug:     var(--s-2);
  --gap-tight:   var(--s-3);
  --gap-normal:  var(--s-4);
  --gap-section: var(--s-5);
  --gap-hero:    var(--s-7);

  /* radius */
  --r-sm:  8px; --r-md: 10px; --r-lg: 14px; --r-xl: 18px; --r-pill: 9999px;

  /* shell width (2026-08-06)
   * Spacing, gap and radius all had scales and stayed consistent. Width
   * and breakpoints never had one, and those are exactly the two
   * dimensions that drifted — the teacher shell alone ended up with two
   * competing "canonical" numbers, 1000 and 1080. So: four shells, and a
   * single governing rule.
   *
   * THE RULE: only .stl-main sets page width. Inner elements may cap
   * measure (in ch) but never shell (in px). Overlays that float above
   * the page — modals, dialogs, the printable worksheet — are the one
   * exemption, because they are not laid out inside the column.
   *
   *   --shell-focus : auth cards, confirm dialogs, single-purpose panels
   *   --shell-read  : picker, quiz, results, review, score, history
   *   --shell-work  : teacher — panels plus a side rail
   *   --shell-data  : admin — tables and database views
   */
  --shell-focus:  440px;
  --shell-read:   760px;
  --shell-work:  1000px;
  --shell-data:  1280px;

  /* measure — how many characters a line of text may run before it gets
   * hard to track back to the next one. These four values were already
   * in the stylesheet as raw numbers in a dozen places; naming them just
   * makes the existing convention enforceable. Unlike shell width, any
   * element may set its own measure — that is the whole point of the
   * distinction. */
  --measure-title: 18ch;  /* h1                         */
  --measure-lede:  56ch;  /* subheads, intro paragraphs */
  --measure-body:  44ch;  /* body copy, notes           */
  --measure-meta:  30ch;  /* captions, truncated labels */

  /* motion
   * --ease-out   : gentle settle for screen transitions
   * --ease-spring: light bounce for "alive" hover states (Trench-inspired)
   * --dur-fast   : :active rebound, removed states
   * --dur-base   : most hover transitions
   * --dur-slow   : decorative reveals (underline grows, background washes)
   */
  --ease-out:    cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.2, 0.6, 0.2, 1);
  --dur-fast: 120ms;
  --dur-base: 200ms;
  --dur-slow: 280ms;
}

/* ===========================================================
 *  LIGHT MODE — token overrides
 * ===========================================================
 *
 * Activated by stamping `data-theme="light"` on <html>. The inline
 * boot script in index.html applies the saved value before first
 * paint so light-mode users don't see a dark flash.
 *
 * Design philosophy:
 *   • Page surface is a slightly-cool off-white (#fafbfd) — pure
 *     #fff feels clinical against the rainbow accents. The same
 *     amount of contrast off the page reads as "less harsh."
 *   • Text is near-black with a faint blue cast (#16181f) — pairs
 *     with the cool surface, avoids the muddy-brown look pure
 *     #000 picks up next to the lavender accent.
 *   • Surface layers darken instead of lighten (the dark theme
 *     lightens by adding white at low opacity; here we add slate at
 *     low opacity to the white). Same depth perception, opposite
 *     direction.
 *   • Hairlines roughly double in opacity vs dark mode — what reads
 *     as a subtle 7% white on near-black needs ~14% slate on
 *     near-white to feel equivalent.
 *   • The lavender accent steps slightly darker (#6963e8 vs #8b86ff)
 *     so it stays legible against white. The rainbow gradient stays
 *     identical — it's bright enough against either surface.
 *   • Status colors flip to their saturated mid-tones (text-only
 *     greens/yellows/reds darken to #16a34a / #ca8a04 / #dc2626)
 *     so they pass contrast on white.
 */
:root[data-theme="light"] {
  --stl-bg-deep:      #f3f4f8;
  --stl-bg:           #fafbfd;
  --stl-bg-2:         #f3f5f9;

  /* Surface tints — same "translucent depth" idea as dark mode but
   * we layer slate over white instead of white over near-black. */
  --stl-surface:      rgba(15, 23, 42, 0.025);
  --stl-surface-2:    rgba(15, 23, 42, 0.045);
  --stl-surface-3:    rgba(15, 23, 42, 0.065);

  /* Hairlines roughly 2x opacity to compensate for the white field. */
  --stl-hairline:     rgba(15, 23, 42, 0.10);
  --stl-hairline-hi:  rgba(15, 23, 42, 0.18);

  /* Text scale flipped — near-black down to faint slate gray. WCAG AA
   * for the muted variants:
   *   --stl-text-muted (#3f4555) on #fafbfd surface ≈ 9.4:1
   *   --stl-text-subtle (#5e6573) ≈ 6.3:1
   *   --stl-text-faint (#7e8493) ≈ 4.5:1 (minimum for normal text)
   * Faint stays at exactly the floor so it reads as "low-emphasis"
   * without being illegible. Earlier values were too washed out
   * (#757b8d, #a0a6b5) — table headers + captions came out near gray
   * fog against the page. */
  --stl-text:         #16181f;
  --stl-text-muted:   #3f4555;
  --stl-text-subtle:  #5e6573;
  --stl-text-faint:   #7e8493;

  /* Accent steps darker so it stays legible on white. The companion
   * --stl-accent-2 matches the new center but holds the blue-violet
   * angle the dark-mode pair had. */
  --stl-accent:       #6963e8;
  --stl-accent-2:     #4a6cd1;
  --stl-accent-soft:  rgba(105, 99, 232, 0.10);
  --stl-accent-line:  rgba(105, 99, 232, 0.40);

  /* Status colors. Two layers:
   *   • --stl-green-text / --stl-yellow-text / --stl-red-text  ← used
   *     as PILL / CHART LABEL text. Already at the darker saturated
   *     mid-tones in dark mode and we keep that here.
   *   • --stl-green / --stl-yellow / --stl-red  ← used as SOLID-COLOR
   *     KPI numbers, status dots, gradient sweeps. In dark mode these
   *     are pastel mid-tones (#4ade80 etc.) which are unreadable as
   *     TEXT against white. Override to the same darker saturated
   *     versions so admin KPIs like "LIVE 2123" stop looking pale.
   *   The visual identity (green = good, yellow = caution, red = bad)
   *   is preserved — just at a richer saturation that pops on white. */
  --stl-green:        #16a34a;
  --stl-yellow:       #ca8a04;
  --stl-red:          #dc2626;
  --stl-green-rgb:    22, 163, 74;
  --stl-yellow-rgb:   202, 138, 4;
  --stl-red-rgb:      220, 38, 38;
  --stl-amber:        #b45309;
  --stl-amber-rgb:    180, 83, 9;
  --stl-amber-text:   #92400e;
  --stl-blue:         #2563eb;
  --stl-blue-rgb:     37, 99, 235;
  --stl-blue-text:    #1d4ed8;
  --stl-rainbow-fill:       #ffffff;
  --stl-rainbow-fill-hover: #f6f7fb;
  --stl-panel: rgba(255, 255, 255, 0.92);
  --stl-hairline-hover: rgba(15, 23, 42, 0.28);
  --stl-green-text:   #15803d;
  --stl-yellow-text:  #a16207;
  --stl-red-text:     #b91c1c;

  /* When a test isn't active, --stl-tint defaults from the dark
   * theme would render too pale against white. JS sets --stl-tint
   * inline on the html element via applyTestAccent — that flows
   * through unchanged; this is only the unset fallback. */
}

/* ---- Light-mode hardcoded-color overrides ----
 *
 * A few rules in this file use hex colors directly instead of the
 * tokens above (typically because they pair specific shades with a
 * specific gradient). Light mode flips those inline. */
:root[data-theme="light"] body,
:root[data-theme="light"] {
  background: var(--stl-bg);
  color: var(--stl-text);
  color-scheme: light;
}
:root[data-theme="light"] .stl-header {
  background: rgba(250, 251, 253, 0.92);
  border-bottom-color: var(--stl-hairline);
}
/* The .stl-outline / picker tile / Tests-row use the
 * `linear-gradient(#15161d, #15161d) padding-box` double-background
 * trick to draw a rainbow border with a dark fill. Light mode swaps
 * the fill to a near-white so the rainbow border still works. */
:root[data-theme="light"] .stl-btn--outline {
  background:
    linear-gradient(#fafbfd, #fafbfd) padding-box,
    var(--stl-rainbow) border-box;
}
:root[data-theme="light"] .stl-btn--outline:hover {
  background:
    linear-gradient(#f0f2f7, #f0f2f7) padding-box,
    var(--stl-rainbow) border-box;
}
/* ============================================================
 *  Rainbow-border focal elements — light-mode fill swap
 * ============================================================
 * Several elements in the app use the padding-box/border-box
 * double-background trick to paint a rainbow border with a solid
 * dark interior (#15161d). That interior is hardcoded for the dark
 * canvas. On light mode (#fafbfd page bg), every one of these
 * renders as a black pill with whatever white text or icon glyph
 * lives inside becoming invisible.
 *
 * The fix is uniform: swap the padding-box fill to white in light
 * mode. The rainbow border stays — it's theme-independent and reads
 * fine on either canvas. Text/icon color is already `var(--stl-text)`
 * in most cases, which auto-flips to dark in light mode.
 *
 * If a NEW element ever adopts the dark+rainbow pattern, add its
 * selector to this group rather than scattering overrides. The
 * elsewhere-fixed pair (.stl-btn--pill-rainbow + the usermenu panel)
 * use `#ffffff` for the same intent — same convention.
 */
:root[data-theme="light"] .stl-header__nav-link[aria-current="page"] .stl-header__nav-icon,
:root[data-theme="light"] .stl-history__chip[data-history-filter="all"].is-active,
:root[data-theme="light"] button.stl-admin__kpi[data-kind="total"][aria-pressed="true"],
:root[data-theme="light"] .stl-admin__tab[aria-selected="true"] .stl-admin__tab-count {
  background:
    linear-gradient(#ffffff, #ffffff) padding-box,
    var(--stl-rainbow) border-box;
}
/* The "All" history chip's count badge has the rainbow as its full
 * background with text colored `var(--stl-bg)`. In dark mode that
 * resolves to #0b0c11 — strong contrast on the bright gradient. In
 * light mode it resolves to #fafbfd — fails contrast against the
 * lighter stops of the rainbow. Force dark text in light mode. */
:root[data-theme="light"] .stl-history__chip[data-history-filter="all"].is-active .stl-history__chip-n {
  color: var(--stl-text);
}
/* @media (hover: none) explicitly re-applies the dark fill to
 * .stl-btn--outline on touch devices to clear the lift/shadow. That
 * undoes our light-mode swap above for touch. Re-apply the light
 * fill inside the same query so touch + light mode behaves. */
@media (hover: none) {
  :root[data-theme="light"] .stl-btn--outline:hover {
    background:
      linear-gradient(#fafbfd, #fafbfd) padding-box,
      var(--stl-rainbow) border-box;
  }
}

/* --primary is now a border-only rainbow rectangle (see the block near
 * `.stl-btn--primary` further down). Its padding-box fill is hardcoded
 * #15161d for the dark canvas, so light mode needs the usual swap to
 * white — same convention as --pill-rainbow and the rest of the
 * rainbow-border group below. Before 2026-08-05 this pair softened the
 * glass rim of the FILLED primary; that treatment is gone. */
:root[data-theme="light"] .stl-btn--primary,
:root[data-theme="light"] .stl-btn--rainbow-outline {
  background:
    linear-gradient(#ffffff, #ffffff) padding-box,
    var(--stl-rainbow) border-box;
  color: var(--stl-text);
  box-shadow: none;
}
/* The picker's "why it works" band uses the same double-background
 * trick and so needs the same swap. It is listed separately rather
 * than added to the selector above because it is not a button: it has
 * no hover fill and no shadow, so folding it in would hand it both. */
:root[data-theme="light"] .stl-whyband {
  background:
    linear-gradient(#ffffff, #ffffff) padding-box,
    var(--stl-rainbow) border-box;
}
:root[data-theme="light"] .stl-btn--primary:hover,
:root[data-theme="light"] .stl-btn--rainbow-outline:hover {
  background:
    linear-gradient(#f6f7fb, #f6f7fb) padding-box,
    var(--stl-rainbow) border-box;
  box-shadow: 0 10px 24px -10px rgba(var(--stl-rainbow-mid-rgb), 0.35);
}
/* Touch devices persist :hover after a tap, so the hover fill would
 * stick. Re-state the resting light fill inside the same query. */
@media (hover: none) {
  :root[data-theme="light"] .stl-btn--primary:hover,
  :root[data-theme="light"] .stl-btn--rainbow-outline:hover {
    background:
      linear-gradient(#ffffff, #ffffff) padding-box,
      var(--stl-rainbow) border-box;
    box-shadow: none;
  }
}

/* ===========================================================
 *  Theme-toggle button (header chip)
 * ===========================================================
 *
 * Lives next to the Highlight toggle. Same ghost-button base; we
 * only override icon visibility based on the current theme. Moon
 * shows in dark mode (so the user clicks "moon → go light"); sun
 * shows in light mode (so user clicks "sun → go dark"). The label
 * text updates from JS to match. */
.stl-theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.stl-theme-toggle__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
}
/* Default theme = dark: show the moon (action = go light). */
.stl-theme-toggle__icon--sun  { display: none; }
:root[data-theme="light"] .stl-theme-toggle__icon--moon { display: none; }
:root[data-theme="light"] .stl-theme-toggle__icon--sun  { display: inline-flex; }

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

/* ====================================================================
 * `hidden` MEANS HIDDEN. Read this before adding another
 * `.thing[hidden] { display: none }` rule — you do not need one.
 *
 * The user-agent stylesheet says `[hidden] { display: none }`, and ANY
 * author `display` declaration beats it, because author styles outrank
 * UA styles regardless of specificity or order. So every component with
 * `display: flex` / `grid` / `inline-flex` in this file silently ignores
 * the hidden attribute, and every one of them is a latent bug the moment
 * JavaScript sets `el.hidden = true` and trusts it.
 *
 * That has shipped at least five separate times: the toast text, the
 * teacher "← Dashboard" button, `.stl-score__target`, the menu-sheet Sign
 * out / Teacher rows visible to guests, and — found by the audit that
 * produced this rule — `#btn-switch-mode`, which was rendering at
 * 186×36px in the account menu while marked hidden. Each was fixed with
 * one more per-element guard; there are ~76 of those in this file, which
 * is the same fix written seventy-six times.
 *
 * This kills the class. `!important` is load-bearing and is the correct
 * tool here: the whole point is to outrank an author `display` on the
 * same element, which normal specificity cannot reliably do.
 *
 * Scoped to descendants of body (the app root class lives ON <body>),
 * which covers overlays appended to body at runtime — the countdown, the
 * print root, modals — not just markup inside the screens.
 *
 * VERIFIED SAFE before adding: no rule anywhere sets a `[hidden]`
 * element to a visible display, so nothing was relying on the bug except
 * the one dead button named above. If you ever genuinely need a hidden
 * element to render, do not fight this rule — that element should not be
 * carrying the hidden attribute.
 * ================================================================== */
.stl-app [hidden] { display: none !important; }

html, body { margin: 0; padding: 0; }
html {
  /* Reserve the gutter, do NOT force a scrollbar (Josh 2026-08-12:
   * "remove the unnecessary vertical scrolling ... it should only
   * scroll if needed"). This used to be `overflow-y: scroll`, which
   * pinned a permanently visible bar on every page, short ones
   * included. scrollbar-gutter alone still holds the space in every
   * engine that supports it (Safari has since 18.2), so the original
   * bug it was guarding — centered layouts shifting sideways when
   * content crossed the viewport height — stays fixed without the
   * always-on scrollbar. */
  scrollbar-gutter: stable;
  /* clip horizontal overflow so the card-slide View Transition never
   * paints a horizontal scrollbar mid-animation when the leaving card
   * is at translateX(-110%) */
  overflow-x: clip;
  /* iOS: prevent text-size auto-bumping when rotating to landscape */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* Touch baseline. `manipulation` removes the 300ms double-tap-zoom
 * delay on iOS Safari for these affordances; the tap-highlight reset
 * kills the gray flash that fires on every tap. Hover states are
 * scoped to hover-capable pointers below so they don't get "stuck"
 * after a touch. */
.stl-btn,
.dc-btn,
.stl-signal__btn,
.stl-choice,
.stl-choice--chart,
.stl-tier,
.stl-stepnav__btn,
.stl-input,
.stl-quiz__gridin-input {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* Anti-banding via EASED GRADIENTS, not noise (Josh 2026-08-10:
 * "remove the noise"). The old approach dithered 8-bit quantization
 * rings with an feTurbulence grain tile; the rings themselves come
 * from two-stop gradients, whose linear alpha ramp has derivative
 * kinks at both ends — exactly where the eye finds Mach bands. Every
 * ambient gradient below now uses ~8 stops following a half-cosine
 * falloff (alpha × 0.5+0.5·cos(πt) at t = 0, .15, .3, .45, .6, .75,
 * .9, 1): each segment's per-pixel delta is a fraction of a
 * quantization step and the curve enters and leaves the ramp with
 * zero slope, so there is no edge to find. No texture, no filter
 * cost. If a ramp ever bands again, add stops — don't re-add grain. */
.stl-app {
  font-family: "Geist", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  font-feature-settings: "ss01", "cv11";
  font-size: 15px;
  line-height: 1.55;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--stl-text);
  background: var(--stl-bg-deep);
  min-height: 100dvh;
  /* Sticky-footer column (Josh 2026-08-11: "there's a lot of extra space
   * at the bottom of pages ... it's adding unnecessary scrollbars").
   * Several screens used to fill the viewport with their own
   * `min-height: 100dvh` (or `100dvh - 60px` under the header). None of
   * them knew the legal footer existed, so the footer's ~122px landed
   * PAST the fold and every one of those screens shipped a scrollbar
   * that revealed nothing but the footer. Laying body out as a column
   * and letting .stl-main take the slack means "fill the viewport" is
   * computed from what's actually left over — header and footer
   * included — instead of from a hardcoded viewport fraction. */
  display: flex;
  flex-direction: column;
  position: relative;
  /* MUST be `clip`, never `hidden`. `overflow-x: hidden` forces the
   * other axis to compute to `auto` (CSS Overflow 3: a visible axis
   * paired with a non-visible/non-clip axis becomes auto), which turns
   * <body> into a real scroll container. Body's overflow would normally
   * propagate up to the viewport instead — but only when <html>'s
   * overflow is `visible`, and ours is `scroll`/`clip` (see the html
   * rule above), so propagation never happens and body keeps a
   * scrollport of its own that never actually scrolls. Every
   * `position: sticky` descendant then scopes itself to that dead
   * scrollport and silently never latches: the site header and the
   * worksheet builder aside both scrolled clean off the top. `clip`
   * pairs legally with `visible`, so the horizontal clipping we want
   * survives while body stops being a scroll container. */
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
}

/* single soft ambient glow, top-right (matches homepage). Pulls
 * --stl-tint so the glow matches the active test (orange for ACT,
 * green for ISEE, etc.) — and stays neutral white on the picker /
 * splash screens where applyNeutralAccent has set --stl-tint to
 * 255, 255, 255. We tone the alpha down a hair for the white state
 * (which reads brighter than colored at the same alpha) via a
 * separate rule below. */
.stl-app::before {
  content: "";
  position: fixed;
  top: -300px;
  right: -200px;
  width: 800px;
  height: 800px;
  /* Zero-alpha same-hue end stop, not bare `transparent` — see the
   * ::after rule below for why. This orb bands less than the others
   * because the 40px blur already dithers it, but the muddying toward
   * black is visible on the tint crossfade. */
  background: radial-gradient(closest-side,
    rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.16)  0%,
    rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.151) 10.5%,
    rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.127) 21%,
    rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.093) 31.5%,
    rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.055) 42%,
    rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.023) 52.5%,
    rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.004) 63%,
    rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0)     70%);
  filter: blur(40px);
  pointer-events: none;
  z-index: 0;
  /* Smooth the white → test-color crossfade when the user enters a
   * test from the picker (and back). Longer than the per-element
   * hover duration since this is a page-scale change. */
  transition: background var(--dur-slow) var(--ease-out);
}
/* Neutral state — pink-purple (Joshua 2026-07-20), blended from the
 * rainbow's violet #a78bfa + pink #f0abfc stops. Only the NEUTRAL
 * (picker/splash) state is pinned; inside a test the orb still
 * crossfades to that test's tint via the rule above. */
html:not(.stl-test-active) .stl-app::before {
  background: radial-gradient(closest-side,
    rgba(212, 140, 250, 0.15)  0%,
    rgba(212, 140, 250, 0.142) 10.5%,
    rgba(212, 140, 250, 0.119) 21%,
    rgba(212, 140, 250, 0.087) 31.5%,
    rgba(212, 140, 250, 0.052) 42%,
    rgba(212, 140, 250, 0.022) 52.5%,
    rgba(212, 140, 250, 0.004) 63%,
    rgba(212, 140, 250, 0)     70%);
}
html[data-theme="light"]:not(.stl-test-active) .stl-app::before {
  background: radial-gradient(closest-side,
    rgba(168, 85, 247, 0.13)  0%,
    rgba(168, 85, 247, 0.123) 10.5%,
    rgba(168, 85, 247, 0.103) 21%,
    rgba(168, 85, 247, 0.075) 31.5%,
    rgba(168, 85, 247, 0.045) 42%,
    rgba(168, 85, 247, 0.019) 52.5%,
    rgba(168, 85, 247, 0.003) 63%,
    rgba(168, 85, 247, 0)     70%);
}
/* Two more ambient orbs (Joshua, 2026-07-20): brand blue-green on the
 * left, deeper brand blue center — both big enough to overlap each
 * other (and reach toward the top-right tint orb). Colors come from
 * the rainbow ramp: teal stop #5eead4, and a deepened cousin of the
 * sky stop (#60a5fa → #2563eb). One full-viewport fixed layer, two
 * radial backgrounds; no filter blur (the soft stops do the work, and
 * a viewport-sized blur would tax Safari compositing). */
.stl-app::after {
  content: "";
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none;
  z-index: 0;
  /* All three orbs sit in one horizontal band along the top, level
   * with the pink top-right orb (whose center lands ~100px into the
   * viewport): teal left corner, deep blue top-center, pink right. */
  /* Three anti-banding measures, in order of how much they matter:
   *
   * 1. Eased (half-cosine) stop ramps — see the note above .stl-app.
   *    This replaced the grain tile 2026-08-10.
   * 2. Zero-alpha stops of the SAME hue instead of bare `transparent`.
   *    `transparent` is rgba(0,0,0,0), so ramping to it interpolates
   *    toward BLACK, which both muddies the colour and adds a third
   *    channel with its own quantization steps to band on.
   * 3. A max radius in px. The rings only showed up at wide widths
   *    because vmax kept stretching the ramp, giving each step more
   *    pixels to be visible across. Past ~1600px wide the orb now
   *    stops growing instead of getting flatter. */
  background:
    radial-gradient(circle min(48vmax, 760px) at 3% 8%,
      rgba(45, 212, 191, 0.15)  0%,
      rgba(45, 212, 191, 0.142) 10.5%,
      rgba(45, 212, 191, 0.119) 21%,
      rgba(45, 212, 191, 0.087) 31.5%,
      rgba(45, 212, 191, 0.052) 42%,
      rgba(45, 212, 191, 0.022) 52.5%,
      rgba(45, 212, 191, 0.004) 63%,
      rgba(45, 212, 191, 0)     70%),
    radial-gradient(circle min(54vmax, 860px) at 50% 6%,
      rgba(59, 130, 246, 0.19)  0%,
      rgba(59, 130, 246, 0.180) 10.5%,
      rgba(59, 130, 246, 0.151) 21%,
      rgba(59, 130, 246, 0.110) 31.5%,
      rgba(59, 130, 246, 0.066) 42%,
      rgba(59, 130, 246, 0.028) 52.5%,
      rgba(59, 130, 246, 0.005) 63%,
      rgba(59, 130, 246, 0)     70%);
}
/* Light theme: SEPARATE tuning, not just dimmed dark values. Diffuse
 * washes vanish on white (peak alpha spread too thin) and the light
 * hero card is near-opaque so nothing glows through it — so the
 * light orbs are TIGHTER (steeper stops, concentrated color, like
 * the top-right orb that already worked) and the blue is wide enough
 * that its flanks tint the gutters around the card. */
:root[data-theme="light"] .stl-app::after {
  /* 2026-07-22 (Joshua): both light orbs read too loud against the
   * white canvas — teal 0.30 → 0.16, center blue 0.22 → 0.12. Same
   * tight geometry, roughly half the ink. */
  /* Same anti-banding measures as the dark rule above (eased stops,
   * same-hue zero-alpha ends, px radius cap). Light bands less (the
   * ramp sits in the top of the 8-bit range where a step is a smaller
   * relative jump) but it still bands, and ramping to bare
   * `transparent` on a white canvas is the worst case for the
   * black-interpolation problem. */
  background:
    radial-gradient(circle min(36vmax, 620px) at 4% 8%,
      rgba(20, 184, 166, 0.16)  0%,
      rgba(20, 184, 166, 0.151) 9.3%,
      rgba(20, 184, 166, 0.127) 18.6%,
      rgba(20, 184, 166, 0.093) 27.9%,
      rgba(20, 184, 166, 0.055) 37.2%,
      rgba(20, 184, 166, 0.023) 46.5%,
      rgba(20, 184, 166, 0.004) 55.8%,
      rgba(20, 184, 166, 0)     62%),
    radial-gradient(circle min(62vmax, 980px) at 50% 6%,
      rgba(37, 99, 235, 0.12)  0%,
      rgba(37, 99, 235, 0.113) 9.3%,
      rgba(37, 99, 235, 0.095) 18.6%,
      rgba(37, 99, 235, 0.069) 27.9%,
      rgba(37, 99, 235, 0.041) 37.2%,
      rgba(37, 99, 235, 0.018) 46.5%,
      rgba(37, 99, 235, 0.003) 55.8%,
      rgba(37, 99, 235, 0)     62%);
}
.stl-app > * { position: relative; z-index: 1; }

/* numeric content gets tabular figures by default */
.stl-app input[type="number"],
.stl-quiz__counter,
.stl-pill {
  font-variant-numeric: tabular-nums;
}

a { color: inherit; text-decoration: none; }

/* --- top bar --------------------------------------------------------- */
.stl-header {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  /* Height grows by the iOS safe-area inset so the bar still measures
   * 60px below the notch. The horizontal padding takes the larger of
   * the design padding or the device-specific safe-area inset (left
   * landscape on a notched phone), so content never tucks under the
   * hardware. */
  height: calc(60px + env(safe-area-inset-top));
  padding-top: env(safe-area-inset-top);
  padding-right: max(clamp(var(--s-4), 4vw, var(--s-7)), env(safe-area-inset-right));
  padding-bottom: 0;
  padding-left:  max(clamp(var(--s-4), 4vw, var(--s-7)), env(safe-area-inset-left));
  /* Higher opacity + lower saturation so the ambient page-glow under
   * .stl-app::before (which carries the active test's tint) doesn't
   * bleed up through the navbar's backdrop blur as a colored cast.
   * Reads as a stable dark surface across all screens / test colors. */
  background: rgba(10, 11, 18, 0.92);
  backdrop-filter: saturate(120%) blur(14px);
  -webkit-backdrop-filter: saturate(120%) blur(14px);
  border-bottom: 1px solid var(--stl-hairline);
}
.stl-header__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
}
.stl-header__titles {
  display: flex;
  align-items: baseline;
  gap: 8px;
  line-height: 1.1;
}
.stl-header__name {
  font-weight: 600;
  font-size: 14.5px;
  letter-spacing: -0.02em;
  color: var(--stl-text);
}
.stl-header__eyebrow {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--stl-text-subtle);
  text-transform: lowercase;
}
/* Left-side header group — brand + primary nav tabs grouped together,
 * with auth/chrome pushed to the right by the parent's space-between.
 * Uses align-items: stretch so the nav can fill the full header height,
 * which is what lets the underline indicator sit on the navbar hairline
 * (the indicator is pinned to the BOTTOM of the link, so the link must
 * extend to the bottom of the header — otherwise it floats mid-air). */
.stl-header__left {
  display: inline-flex;
  align-items: stretch;
  /* Self-stretch is critical: the parent .stl-header is
   * `align-items: center`, which would shrink this group to its
   * content height and leave the nav link's "bottom" floating in the
   * middle of the navbar. align-self: stretch overrides that so the
   * link runs floor-to-ceiling and the underline indicator lands on
   * the navbar's actual hairline. */
  align-self: stretch;
  gap: var(--s-5);
  /* Allow the group to shrink so the right-side actions stay reachable
   * if a long user email needs room. The nav itself can ellipsize/hide
   * via the breakpoint rules below if the bar gets tight. */
  min-width: 0;
}
/* Brand link sits inside the stretched container — center it vertically
 * so the wordmark stays at the visual midline despite the stretch. */
.stl-header__brand { align-self: center; }
/* Primary nav — text-only tab strip that sits to the right of the
 * wordmark. Each link is a button (calls into showScreen rather than
 * navigating a URL), styled like a desktop nav tab with a hairline
 * underline indicator on the active page. */
.stl-header__nav {
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  /* Anchor for the sliding indicator; the bar is absolutely positioned
   * inside the nav so it can move between tabs independently. */
  position: relative;
}
.stl-header__nav-link {
  /* Reset button chrome — these are plain text tabs, not pills. */
  appearance: none;
  background: transparent;
  border: 0;
  font: inherit;
  cursor: pointer;
  position: relative;

  display: inline-flex;
  align-items: center;
  gap: 9px;                /* spacing between leading icon and label */
  /* Stretch to full header height so the sliding indicator, pinned to
   * the nav's bottom edge, lands right on the navbar's hairline. */
  align-self: stretch;
  padding: 0 var(--s-3);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
  color: var(--stl-text-muted);
  transition: color var(--dur-base) ease;
  /* Never wrap. .stl-header__left carries min-width: 0 so the group can
   * shrink, and a shrinking flex item lets its text collapse to
   * min-content — the longest WORD. That is why the tabs folded into
   * two lines inside a fixed-height bar instead of overflowing
   * visibly (Josh 2026-08-11). With nowrap the bar fails loudly at the
   * tier boundaries below, where we can actually see and fix it. */
  white-space: nowrap;
}
/* Short label — swapped in for the full one at the tightest desktop
 * tier. Both spans stay in the a11y tree; each nav button carries an
 * explicit aria-label so its accessible name never changes with the
 * viewport. */
.stl-header__nav-label-sm { display: none; }
.stl-header__nav-link[hidden] { display: none; }
/* Rounded-square chip behind each icon. Anchors the icon as a visual
 * unit and gives the nav a more app-shell feel than naked glyphs.
 * Default state is a muted surface with a hairline; hover lifts the
 * surface a step, and the active tab fills with the accent tint so
 * the chip becomes a second indicator that pairs with the underline. */
.stl-header__nav-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  color: var(--stl-text-muted);
  /* Tween bg+border+color so the chip flows into its active state in
   * step with the rest of the tab transition. */
  transition:
    background  var(--dur-base) ease,
    border-color var(--dur-base) ease,
    color       var(--dur-base) ease,
    box-shadow  var(--dur-base) ease;
}
.stl-header__nav-icon svg {
  display: block;
  flex-shrink: 0;
}
.stl-header__nav-link:hover .stl-header__nav-icon {
  background: var(--stl-surface-2);
  border-color: var(--stl-hairline-hi);
  color: var(--stl-text);
}
.stl-header__nav-link[aria-current="page"] .stl-header__nav-icon {
  /* Gradient-border treatment for the active tab's icon chip — the
   * padding-box / border-box trick, 1.5px transparent border filled by the
   * rainbow (which signal-theme.css retunes to the green→yellow→red signal
   * ramp), opaque interior so the icon glyph stays legible inside.
   *
   * History, so this does not get re-litigated from scratch: this went GREY
   * on 2026-08-18 at Joshua's request, then came back the same day after he
   * saw the alternatives side by side (`redesigns/nav-selected/`). The grey
   * version and three hard-stop variants are all still on that page if the
   * question reopens. Reverted deliberately, not by accident. */
  border: 1.5px solid transparent;
  background:
    linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
    var(--stl-rainbow) border-box;
  color: var(--stl-text);
  box-shadow: none;
}

.stl-header__nav-link:hover {
  color: var(--stl-text);
}
/* FIXLIST P0 (2026-08-18): this rule used to be `:hover, :focus-visible
 * { outline: none }` — the primary nav actively deleted its focus ring on
 * every screen, the only place in the codebase that removed one. */
.stl-header__nav-link:focus-visible {
  color: var(--stl-text);
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
/* Active tab text — the underline itself lives on the shared indicator
 * below, so this rule only handles the text color promotion. */
.stl-header__nav-link[aria-current="page"] { color: var(--stl-text); }

/* Sliding active-tab underline. A single shared bar absolutely
 * positioned at the bottom of the nav; JS measures the active tab and
 * sets `transform: translateX(...)` + `width: ...px`. The transition
 * gives the spring-out ease most tab strips use (overshoot a touch,
 * settle). Opacity gates first paint so it doesn't slide in from x=0
 * before JS has measured the active tab. */
.stl-header__nav-indicator {
  position: absolute;
  bottom: -1px;                       /* sit on top of the header border */
  left: 0;
  height: 2px;
  width: 0;
  /* Rainbow accent for the navbar's active-tab marker. The navbar is global
   * chrome (not bound to any single test), so the brand's "all tests"
   * gradient is the right default here. Briefly grey on 2026-08-18 — see the
   * note on the icon chip above and `redesigns/nav-selected/` for the
   * variants that were compared. */
  background: var(--stl-rainbow);
  border-radius: 0;
  opacity: 0;
  pointer-events: none;
  transform: translateX(0);
  /* Transitions only kick in once .is-ready lands. Before that the
   * indicator is in its "first positioning" phase and snaps directly
   * to its target — without this, the very first paint after page
   * load would slide the bar from x=0 (the CSS default) to its real
   * position while opacity was rising, reading as a visible flash. */
  transition: none;
}
.stl-header__nav-indicator.is-ready {
  opacity: 1;
  /* easeOutCubic — pure deceleration, no overshoot. The previous
   * cubic-bezier(0.32, 0.72, 0, 1) had a faint rubber-band feel on
   * arrival that read as a "bounce" on tab clicks. This curve
   * settles flat. */
  transition:
    transform 280ms cubic-bezier(0.33, 1, 0.68, 1),
    width     280ms cubic-bezier(0.33, 1, 0.68, 1),
    opacity   180ms ease;
}
/* Honor reduced motion — no slide, just instant placement. */
@media (prefers-reduced-motion: reduce) {
  .stl-header__nav-indicator,
  .stl-header__nav-indicator.is-ready {
    transition: opacity 120ms ease;
  }
}
/* Self-click bounce: when the user clicks the tab they're already on,
 * showScreen would no-op (same → same), which feels dead. JS adds
 * .is-bouncing for a beat so the underline gives a small "yep, here"
 * pulse using a spring curve. The animation runs on top of the
 * existing translateX, so we use a CSS variable to compose:
 * indicator's transform already includes translateX(Xpx); we add the
 * scaleY pulse via a sibling animation property. */
@keyframes stl-nav-indicator-bounce {
  0%   { transform: translateX(var(--nav-indicator-x, 0)) scaleY(1)   scaleX(1); }
  35%  { transform: translateX(var(--nav-indicator-x, 0)) scaleY(2.2) scaleX(0.92); }
  70%  { transform: translateX(var(--nav-indicator-x, 0)) scaleY(0.7) scaleX(1.06); }
  100% { transform: translateX(var(--nav-indicator-x, 0)) scaleY(1)   scaleX(1); }
}
.stl-header__nav-indicator.is-bouncing {
  /* Spring curve — peak occurs near 35%, settles in two damped
   * half-cycles. The animation overrides the inline transform for
   * its duration; the CSS variable carries the translateX position
   * so the bounce composes cleanly with the current slide offset. */
  animation: stl-nav-indicator-bounce 360ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  transform-origin: center bottom;
}
@media (prefers-reduced-motion: reduce) {
  .stl-header__nav-indicator.is-bouncing { animation: none; }
}

.stl-header__actions { display: flex; gap: var(--s-2); align-items: center; }

/* New Test header CTA — feature-styled action button anchored on the
 * right side of the navbar. Inherits the primary-button gradient/glow
 * (already themed by --stl-accent) but at a compact header scale so it
 * doesn't dominate the chrome. The leading + icon visually distinguishes
 * it from the text-only nav tabs on the left. */
.stl-header__cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* Symmetric horizontal padding — the visual lopsidedness at smaller
   * paddings comes from the icon being narrower than the trailing
   * text. 16px each side + 8px gap reads balanced even with the
   * narrower icon on the left. Slightly compact vertical padding to
   * keep the button at navbar scale. */
  padding: 8px 16px;
  font-size: 13px;
  min-height: 0;             /* override mobile .stl-btn min-height */
  letter-spacing: -0.005em;
  white-space: nowrap;       /* "New test" must not fold — see nav-link */
}
.stl-header__cta svg { flex-shrink: 0; display: block; }
@media (max-width: 560px) {
  /* On phones the splash screen owns the entry point, so this button
   * just adds chrome — keep it visible but tighten its footprint so
   * it doesn't fight the auth pill for space. */
  .stl-header__cta { padding: 9px 14px; min-height: 0; }
}

/* Header math-color toggle. Two states:
 *   aria-pressed="true"  → highlighting ON (default). Dot fills with
 *     accent color and the label sits in the canvas text color.
 *   aria-pressed="false" → highlighting OFF. Dot is hollow + muted,
 *     the label drops to text-subtle so the whole control reads
 *     visually inactive without changing layout.
 * The label is hidden on phones to save header room — the dot still
 * reads as a toggle indicator and the aria-label keeps it accessible. */
.stl-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
}
.stl-toggle__dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  background: transparent;
  transition: background  var(--dur-base) ease,
              border-color var(--dur-base) ease,
              box-shadow  var(--dur-base) ease;
  flex-shrink: 0;
}
.stl-toggle[aria-pressed="true"] .stl-toggle__dot {
  background: var(--stl-accent);
  border-color: var(--stl-accent);
  box-shadow: 0 0 6px var(--stl-accent-line);
}
.stl-toggle[aria-pressed="false"] {
  color: var(--stl-text-subtle);
}
@media (max-width: 640px) {
  .stl-toggle__lbl { display: none; }
  .stl-toggle { padding: 6px; }
}

/* Live elapsed-time pill — sits next to the Restart button in the
 * sticky header, visible only while a test is in progress. Mono font
 * with tabular numerals so the digits don't shift width on tick.
 * The leading dot pulses subtly to telegraph "this is live, not a
 * static label." */
.stl-header__timer {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border-radius: var(--r-pill);
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
  font-family: var(--stl-font-mono);
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--stl-text-muted);
  font-variant-numeric: tabular-nums;
}
.stl-header__timer[hidden] { display: none; }
.stl-header__timer::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--stl-accent);
  box-shadow: 0 0 8px var(--stl-accent);
  animation: stl-timer-pulse 2s ease-in-out infinite;
}
@keyframes stl-timer-pulse {
  0%, 100% { opacity: 0.45; }
  50%       { opacity: 1; }
}
@media (max-width: 560px) {
  .stl-header__timer { font-size: 12px; padding: 4px 10px; }
  /* On narrow phones the brand subtitle ("aidroplets") already hides;
   * keep the timer visible because it's the most useful header item
   * during a test. */
}
@media (prefers-reduced-motion: reduce) {
  .stl-header__timer::before { animation: none; opacity: 0.85; }
}

/* ── Brand mark (studysignal wordmark + traffic-light dots) ──
   Replaces the legacy .stl-logo dot-pill. SVG ships inline in index.html;
   text uses currentColor so it inherits via .stl-brand color. */
.stl-brand {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  color: var(--stl-text);
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-base) var(--ease-out);
}
.stl-brand:hover { color: var(--stl-text); }
.stl-brand:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 4px;
  border-radius: 4px;
}
.stl-brand__svg {
  display: block;
  height: 28px;
  width: auto;
  color: var(--stl-text);
}
.stl-brand__svg--lg { height: 40px; }

/* --- main / screens --------------------------------------------------- */
.stl-main {
  max-width: var(--shell-read);
  /* width:100% is load-bearing, not belt-and-braces. main is a flex item
   * of the body column now, and a flex item with auto side margins does
   * NOT stretch to the cross axis — the auto margins win and it
   * shrink-wraps its content instead. That made the shell width follow
   * whatever happened to be on screen: the teacher shell measured 1000px
   * on the dashboard but 401px on the Manage tabs and 743px on the
   * worksheet builder, so the H1 and the whole column slid sideways on
   * every navigation (Josh 2026-08-11: "the width is jumping around for
   * each of the teacher pages"). Asking for the full width explicitly
   * restores the pre-flex behavior; max-width still caps it and the auto
   * margins still center it. */
  width: 100%;
  margin: 0 auto;
  /* Bottom padding takes the larger of the design value or the iOS
   * home-indicator safe area, so the last row of buttons never sits
   * under the indicator on a notched phone. Side padding takes the
   * larger of the design value or the landscape safe-area insets. */
  padding-top: clamp(var(--s-6), 5vw, var(--s-8));
  padding-right: max(clamp(var(--s-4), 4vw, var(--s-7)), env(safe-area-inset-right));
  /* Was clamp(--s-7, 6vw, 64px). Together with the footer's own 40px
   * top margin that left a ~104px void between the last control and
   * the legal line, and pushed short pages over the fold (Josh
   * 2026-08-12). Half the padding, same breathing room. */
  padding-bottom: max(clamp(var(--s-5), 3vw, 36px), env(safe-area-inset-bottom));
  padding-left:  max(clamp(var(--s-4), 4vw, var(--s-7)), env(safe-area-inset-left));
  display: grid;
  /* Take the slack in body's column so main fills whatever is left
   * between the header and the footer. Grow but never shrink: content
   * taller than the viewport still pushes the footer down normally. */
  flex: 1 0 auto;
  transition: max-width var(--dur-base) var(--ease-out);
}
/* Admin is a database view — it benefits from horizontal real estate
 * the way reading-focused screens (quiz, results, review) don't. We
 * widen .stl-main only when the admin screen is mounted, leaving the
 * rest of the app at its tighter reading column. */
.stl-main:has([data-screen="admin"]:not([hidden])) {
  max-width: min(var(--shell-data), calc(100vw - 32px));
}
/* ONE canonical width for the ENTIRE teacher shell (Josh 2026-07-26):
 * previously main was 760px for most teacher panels but jumped to
 * 1280px when the worksheets panel mounted (and the shell capped
 * itself at 880), so the H1 shifted sideways on every tab change and
 * again when empty states loaded in. Now the teacher screen always
 * gets the same main width — no panel-dependent :has() — and this
 * declaration is the single number that governs title, subhead, and
 * every panel in both empty and populated states. .stl-teacher itself
 * carries no max-width (2026-08-06); see the note on that rule. */
.stl-main:has([data-screen="teacher"]:not([hidden])) {
  max-width: min(var(--shell-work), calc(100vw - 32px));
}
/* min-width: 0 is the phone-width firewall (Josh 2026-08-25, Android:
 * "some things didn't fit nicely"). .stl-main is a grid, so every
 * screen is a grid item, and a grid item's default min-width:auto means
 * ONE unbreakable descendant widens the entire page. That is exactly
 * what happened: a 405px-wide step-viz row inside the how screen forced
 * every section to 455px at a 360px viewport — content pinned wider
 * than the phone, uniformly, on every block. With min-width:0 the
 * screen honors the shell width and an oversized element overflows its
 * OWN box, where it is visible and locally fixable, instead of silently
 * widening all of them. */
.stl-screen { display: grid; gap: var(--s-6); min-width: 0; }
/* Rows must NOT absorb the screen's leftover height. main is body's
 * flex-grow child, so it fills the viewport; its grid row stretches the
 * screen, the screen's row stretches the card, and — because most cards
 * ARE grids (.stl-results, .stl-score, .stl-history, .stl-learn) — the
 * slack gets dealt out to their auto rows and every child inflates.
 * That is the whole family of "why is this button HUGE on refresh"
 * bugs: it bites hardest on first paint, when a data row is still
 * hidden and the buttons are the only rows left to absorb the height
 * (Josh 2026-08-11, Analysis then Results). Packing rows to the top
 * makes each one its natural height and leaves the slack at the bottom
 * of the screen where it belongs. --auth is excluded: it centers its
 * card in the viewport on purpose via place-items. */
.stl-screen:not(.stl-screen--auth) { align-content: start; }
.stl-screen[hidden] { display: none; }

/* --- shared card ------------------------------------------------------ */
.stl-card {
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  padding: clamp(var(--s-5), 3vw, var(--s-7));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.stl-card--hero {
  /* the only "lift" surface in the app */
  background:
    linear-gradient(165deg, rgba(148, 163, 184, 0.06) 0%, rgba(255, 255, 255, 0.02) 50%, transparent 80%),
    var(--stl-surface);
  border-color: var(--stl-hairline-hi);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.05),
    0 1px 0 rgba(0, 0, 0, 0.35),
    0 18px 50px -12px rgba(0, 0, 0, 0.45);
}
/* Light-mode override — the dark hairline (rgba(0,0,0,0.35)) reads as
 * a harsh underline on white. Soften the bottom edge: a near-invisible
 * 1px shadow + a much lighter, more diffused drop shadow that's tuned
 * for the white canvas. The inset highlight gets bumped because pure
 * white needs more separation against the card surface. */
:root[data-theme="light"] .stl-card--hero {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.6),
    0 1px 2px rgba(0, 0, 0, 0.04),
    0 12px 32px -16px rgba(15, 23, 42, 0.12);
}

/* --- score screen ----------------------------------------------------- */
.stl-score { display: grid; gap: var(--gap-section); }

.stl-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--stl-font-mono);
  font-size: 11px;
  /* One authority for the letterspaced-subtitle format (Josh
   * 2026-08-11: some were caps, some weren't). Uppercase lives HERE,
   * not in the copy, so source strings stay sentence case and every
   * eyebrow — "All-time topic mastery", "Strengths", "Weaknesses",
   * "Explanation" — renders identically. Tracking is 0.12em because
   * caps need more air than the old 0.04em gave them. Don't re-declare
   * letter-spacing or text-transform on individual eyebrows. */
  text-transform: uppercase;
  letter-spacing: 0.12em;
  /* Weight lives here too. Two eyebrows are <h3> (the Results and
   * Analysis radar titles) and were inheriting the user-agent's bold
   * while every <p>-based eyebrow sat at the body's 400 — same class,
   * two weights, depending on which element it happened to land on
   * (Josh 2026-08-11). Pinning it means the tag never leaks through.
   * Everything in this block is the format: font, size, weight,
   * tracking, case. Scoped overrides may re-declare COLOR only. */
  font-weight: 400;
  color: var(--stl-text-subtle);
  margin: 0;
}
.stl-eyebrow::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  /* Per-instance overrides via --eyebrow-dot-bg and --eyebrow-dot-glow
   * let a specific eyebrow track a different signal than the page's
   * global accent (e.g. the AI Analysis radar title tracks the
   * filter chip, not the active test). Defaults preserve the
   * original "accent-tinted" look. */
  background: var(--eyebrow-dot-bg, var(--stl-accent));
  /* Glow tracks the current accent — purple by default, white on the
   * picker (no test selected), or the test's color on score/quiz.
   * Transition both background and shadow so the recolor on test
   * change reads as a smooth fade rather than a snap. */
  box-shadow: 0 0 8px var(--eyebrow-dot-glow, rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.55));
  transition: background var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out);
}
/* When --eyebrow-dot-bg is set to the brand rainbow gradient, KEEP
 * the dot a clean 6×6 circle (matching every other eyebrow dot in
 * the app). The earlier pill experiment looked broken next to the
 * solid round dots on other filter chips. */
.stl-eyebrow--rainbow-dot::before {
  width: 6px;
  height: 6px;
  border-radius: 50%;
}

.stl-h1 {
  margin: 0;
  font-family: var(--stl-font-display);
  font-size: clamp(1.9rem, 4.5vw, 2.75rem);
  letter-spacing: -0.025em;            /* Space Grotesk wants slightly less tight tracking than Geist */
  font-weight: 600;
  line-height: 1.05;
  color: var(--stl-text);
  max-width: var(--measure-title);
}

/* Inline empty-state for the score screen — shown when the
 * currently-selected test has no live questions yet. Replaces the
 * score input form with a friendly message + a "Pick a different
 * test" link (when other tests are Active). Padding + gap match
 * the surrounding card chrome so it doesn't feel orphaned. */
.stl-score__empty {
  display: grid;
  gap: var(--s-3);
  padding: var(--s-4) 0;
}
.stl-score__empty[hidden] { display: none; }

/* Inline back link — used at the top of the score screen card to
 * route back to the picker when 2+ tests are Active. Subtle by
 * design (just text + a chevron) so it doesn't compete with the
 * primary headline. Picks up accent color on hover. */
.stl-back-link {
  appearance: none; -webkit-appearance: none;
  background: transparent; border: 0; padding: 0;
  font: inherit; color: var(--stl-text-muted);
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px;
  font-weight: 500;
  margin-bottom: var(--s-2);
  margin-left: -2px;
  border-radius: var(--r-sm);
  transition: color var(--dur-base) ease;
}
.stl-back-link:hover {
  color: var(--stl-accent);
}
.stl-back-link:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 4px;
}
.stl-back-link svg {
  flex-shrink: 0;
  transition: transform var(--dur-fast) var(--ease-spring);
}
.stl-back-link:hover svg {
  transform: translateX(-2px);
}

/* Gradient text for the test-name token in the score-screen headline.
 * Lavender → blue diagonal sweep matching the rest of the v2 palette.
 * Uses background-clip: text (well-supported in modern browsers,
 * gracefully degrades to flat accent color via the fallback rule). */
.stl-grad-text {
  color: var(--stl-accent);                         /* fallback */
  background: linear-gradient(120deg, var(--stl-accent) 0%, var(--stl-accent-2) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  font-weight: 700;        /* a notch heavier so the test name pops */
  letter-spacing: -0.025em;
}

/* ============== TEST PICKER GRID ==============
 * Shown when 2+ tests are Active. Each tile is a click-to-start
 * shortcut — the wordmark dominates the visual hierarchy, with the
 * test name + tagline below. Hover lifts the tile and tints the
 * border with the test's --tile-tint var (set inline per tile). */
.stl-picker {
  /* Same visual chrome as .stl-score so the picker feels like a
   * sibling entry-step, not a separate UI mode. */
  display: grid;
  gap: var(--s-4);
}
.stl-picker__grid {
  display: grid;
  /* Wider min so the side-by-side icon + text layout doesn't get
   * cramped at narrower viewports. */
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--s-4);
  margin-top: var(--s-3);
}
.stl-picker__tile {
  appearance: none; -webkit-appearance: none;
  font: inherit; color: inherit; text-align: left;
  cursor: pointer;
  /* Side-by-side layout: icon on the left at its natural size,
   * meta (name + tagline) fills the rest. align-items:center
   * vertically aligns the icon's midpoint with the text block. */
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background: var(--stl-surface);
  position: relative;
  overflow: hidden;
  transition:
    transform var(--dur-fast) var(--ease-spring),
    border-color var(--dur-base) var(--ease-spring),
    box-shadow var(--dur-base) var(--ease-spring);
}
.stl-picker__tile::before {
  /* Soft tinted glow per test — uses the inline --tile-tint
   * (e.g., "139, 134, 255" for SAT, "74, 222, 128" for ISEE). */
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.10), transparent 60%);
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-spring);
}
.stl-picker__tile:hover {
  transform: translateY(-2px);
  border-color: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.55);
  box-shadow: 0 12px 28px -14px rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.45);
}
.stl-picker__tile:active { transform: translateY(0); }
.stl-picker__tile:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 3px;
}
.stl-picker__mark {
  position: relative;
  /* Side-by-side layout in the tile — icon at 64px reads as the
   * dominant element next to the acronym + tagline without
   * crowding the text. */
  display: flex;
  align-items: center;
  --stl-mark-icon-size: 64px;
}
.stl-picker__tile {
  /* Color cascade: each tile applies its own tint via color, which
   * the inner currentColor SVG fills inherit. */
  color: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 1);
}
/* Shared wordmark layout used by the picker tile and the admin
 * Tests panel rows. Icon stacks above the acronym in both contexts. */
.stl-test-mark {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  line-height: 1;
}
.stl-test-mark__icon {
  display: inline-flex;
  width: var(--stl-mark-icon-size, 32px);
  height: var(--stl-mark-icon-size, 32px);
  /* Color is inherited from the parent (the picker tile or Tests
   * row), which sets it to the test's tint. The SVGs use
   * fill="currentColor" so they pick up that color. */
  color: inherit;
}
.stl-test-mark__icon svg {
  width: 100%;
  height: 100%;
  display: block;
}
.stl-picker__meta {
  position: relative;
  display: grid;
  gap: 4px;
}
.stl-picker__name {
  /* The only place the test acronym appears on the tile (the
   * wordmark dropped its inline acronym to avoid duplication).
   * Tinted to match the tile's --tile-tint so it visually pairs
   * with the icon. Uses the display font for heading-tier weight. */
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  font-family: var(--stl-font-display);
  color: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 1);
}
.stl-picker__tagline {
  font-size: 0.875rem;
  color: var(--stl-text-muted);
}
@media (max-width: 640px) {
  .stl-picker__grid { grid-template-columns: 1fr; }
  .stl-picker__tile { padding: var(--s-4); }
}
.stl-sub {
  margin: 0;
  color: var(--stl-text-muted);
  font-size: 1rem;
  line-height: 1.6;
  max-width: var(--measure-lede);
}

.stl-score__form { display: grid; gap: var(--s-4); margin-top: var(--s-2); }
.stl-field { display: grid; gap: var(--s-2); }
.stl-label {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  font-weight: 400;          /* pin it — otherwise .stl-label on an <h3>
                                inherits the UA bold while on a <span> it
                                reads 400, which looked inconsistent. */
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-input {
  width: 100%;
  padding: 14px 16px;
  font-size: 1.25rem;
  font-weight: 500;
  font-family: inherit;
  color: var(--stl-text);
  background: var(--stl-bg);
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-md);
  outline: none;
  transition: border-color var(--dur-base) var(--ease-out),
              background  var(--dur-base) var(--ease-out),
              box-shadow  var(--dur-base) var(--ease-out);
}
.stl-input:hover { border-color: var(--stl-hairline-hover); }
.stl-input:focus-visible,
.stl-input:focus {
  border-color: var(--stl-accent);
  background: var(--stl-bg-2);
  box-shadow: 0 0 0 4px var(--stl-accent-soft);
}
.stl-hint {
  font-size: 13px;
  color: var(--stl-text-subtle);
  /* Zero the browser's default <p> margins so a hint paragraph
   * doesn't add ~16px of space above + below itself. The CSS grid
   * `gap` on the parent section handles the rhythm; native margins
   * just pile on extra padding inside subcards. */
  margin: 0;
}
.stl-hint.is-error { color: var(--stl-red); }

/* Tier shortcut buttons — clicking one fills the score input with
 * that tier's top-of-range score and applies the active treatment.
 *
 * Desktop: a 4-up grid of card-shaped tiles, similar visual weight
 * to the test-type picker tiles. Each tile has the tier name as the
 * dominant text + the score range as a quiet subtitle below.
 *
 * Mobile: collapses to a horizontal pill row (existing behavior).
 * Reuses the same DOM, just restacks via CSS. The .__range span
 * stays visible but inlines next to .__name in the pill. */
.stl-score__tiers {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-2);
}

/* Length picker: lay ALL options (incl. Custom) on a single equal-width
   row regardless of how many there are, instead of wrapping after 4.
   Desktop only — the mobile rule below keeps the 2-up wrap. */
@media (min-width: 641px) {
  #score-length {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
  }

  /* Difficulty: Tailored sits IN the row with the four tiers rather than on
   * its own full-width line below (Joshua 2026-08-17).
   *
   * Scoped to #score-tiers and to desktop on purpose. The mobile layout
   * (@media max-width 640) is a flex wrap where Tailored takes `flex: 1 0
   * 100%` to hold its own line, and at phone width five tiles across would
   * be unreadable — leaving that block alone is the whole reason this is a
   * min-width block instead of a specificity fight. The teacher composer
   * (#composer-tiers) renders its own tiers and is untouched.
   *
   * Measured before shipping, at the real --shell-read 760px: five equal
   * columns give 133px tiles, one row, "Auto adjusted" needs 78.2px of the
   * 99px available — 20.8px of slack. (The first measurement of that used
   * scrollWidth, which on a block element equals its content box whenever
   * the text fits and so reported zero slack; a Range around the text node
   * gives the real number.)
   *
   * The original full-width treatment existed so Tailored could not "read as
   * a fifth difficulty". That still holds here without the extra row,
   * because it is the only tile with no colour tint and no numeric range,
   * and the only one carrying the signal dots. A hairline separator was
   * tried and dropped: invisible at this size, and complexity for nothing.
   */
  #score-tiers { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  #score-tiers .stl-tier--tailored { grid-column: auto; }
  /* 9.5px/0.1em fitted the old full-width tile; in a fifth of the row it
   * wants the slack back. */
  #score-tiers .stl-tier--tailored .stl-tier__range {
    font-size: 9px;
    letter-spacing: 0.05em;
  }
}

/* "Only this difficulty" switch row — sits between the tier pills
 * and the score input. Two-line layout: switch + label on top, one-
 * line hint below explaining the current state. The switch reuses
 * .stl-switch from the admin Tests panel for visual consistency. */
.stl-score__mix {
  display: grid;
  gap: 4px;
  padding: var(--s-2) 0;
}
.stl-score__mix-switch {
  /* Override the admin context's tile tint — the score-screen mix
   * switch uses the canvas accent so it harmonizes with the gradient
   * test name + the primary submit button. */
  --tile-tint: 74, 222, 128;
  align-self: start;
  padding-left: 0;
}
.stl-score__mix-hint {
  margin: 0;
  /* Keep the hint inline with the switch's left edge for a clean
   * vertical line — no extra indentation since the switch label
   * already starts flush at the left. */
}

/* ---- Score screen sectioning -------------------------------------
 * The score form is split into two visually-grouped sections:
 *   1. Subjects   — pick what to study (the "what")
 *   2. Difficulty — pick how hard it should be (the "how hard"),
 *                   bundling tier pills + the include-lower toggle +
 *                   the target-score input under one heading
 * Each section renders as a labeled block with a consistent header
 * row so they read as parallel decisions, not stacked controls. */
.stl-score__section {
  /* Each section reads as its own bordered subcard inside the main
   * score card. Visual hierarchy: outer card = the screen, inner
   * cards = the two grouped decisions (Subjects + Difficulty).
   * Toggle + target-score live INSIDE the Difficulty card so they
   * visually belong to it. */
  display: grid;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-4);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  background: var(--stl-surface);
}
.stl-score__section[hidden] { display: none; }
.stl-score__section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
}
/* Within the difficulty section, the tier pills, mix toggle, and
 * target-score input read tighter together than the cross-section
 * gap. The mix toggle gets a top border so it visually anchors as
 * a sub-section of Difficulty (not a third floating control). */
.stl-score__difficulty .stl-score__mix {
  padding: var(--s-3) 0 0;   /* zero the inherited 8px bottom — the section's own padding handles it */
  border-top: 1px solid var(--stl-hairline);
}
.stl-score__difficulty .stl-score__mix + .stl-score__target,
.stl-score__difficulty .stl-score__tiers + .stl-score__target {
  padding-top: var(--s-3);
  border-top: 1px solid var(--stl-hairline);
}
.stl-score__target { display: grid; gap: var(--s-2); }

/* ---- Subject picker on the score screen ---------------------------
 * A pill row that lets the user enable/disable each subject the test
 * covers (e.g. ACT: English, Math, Reading, Science). Pills carry an
 * icon + label and use aria-pressed (toggle), not aria-selected
 * (single choice), since multiple can be on at once. */
/* Selectors chain .stl-link.<class> for specificity (0,2,0) so they
 * beat .stl-link (0,1,0). Without the chain, .stl-link's `font: inherit`
 * shorthand wins source order and the link renders at the inherited
 * body 15px regular Geist instead of the 11px mono medium we want. */
.stl-link.stl-score__subjects-toggle,
.stl-link.stl-score__custom-toggle,
.stl-score__subjects-toggle,
.stl-score__custom-toggle {
  font-size: 11px;
  font-family: var(--stl-font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--stl-text-subtle);
}
.stl-link.stl-score__subjects-toggle:hover,
.stl-link.stl-score__custom-toggle:hover {
  color: var(--stl-accent);
}
/* Target-score "Set custom score" reveal — sits in the difficulty
 * section header next to the label. Hidden by default, toggleable.
 * The wrapper [hidden] rule is needed because the field grid CSS
 * (display: grid) wins over the user-agent [hidden] selector. */
.stl-score__target[hidden] { display: none; }

/* Back-to-picker link below the Start CTA — centered, muted,
 * reads as a tertiary action under the primary one. Tightened:
 * smaller font, no extra padding, gap pulled in so the chevron
 * sits flush against the label. */
.stl-score__back-foot {
  justify-self: center;
  margin: 0 auto;
  padding: 0;
  gap: 4px;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--stl-text-subtle);
  line-height: 1.2;
}
.stl-score__back-foot svg { width: 12px; height: 12px; }
.stl-score__back-foot:hover { color: var(--stl-text); }

/* ── Paired action row (Josh 2026-08-07, back-button standardization) ──
 * Back stops being an 11.5px footer link tucked under the CTA and
 * becomes a real button sitting BESIDE the primary action, equal width.
 *
 * The chosen treatment is "equal · ghost back": equal footprint,
 * unequal fill. Hierarchy lives in the background, not the geometry —
 * so back reads as a legitimate choice rather than an escape hatch,
 * while the rainbow CTA still obviously owns the screen. Sizing is
 * pinned to .stl-radar__cta.stl-btn--pill-rainbow (13px/14px/pill) so
 * the two halves are the same height without a magic number.
 *
 * THE RULE, and it is the whole design:
 *   exactly ONE primary action on the screen  → .stl-actionpair (2-up)
 *   zero, or two or more primary actions      → .stl-actionpair--solo
 *                                               (full width, LAST element
 *                                               of the action block)
 * The solo form exists so back is never paired with a destructive
 * sibling — on `regen` the 2-up form would sit "Back to results" next
 * to "Start over with a new score" at identical weight. Position stays
 * constant either way (end of the action block), which is what keeps
 * the control learnable across screens.
 *
 * Destination is never written in the markup: [data-back] carries no
 * target, and applyBackLabels() in app.js resolves it through
 * SCREEN_PARENT + SCREEN_TITLE at render time. One label formula,
 * one routing table. */
.stl-actionpair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-2);
  align-items: stretch;
  margin-top: var(--s-5);
}
.stl-actionpair--solo { grid-template-columns: 1fr; }
/* --stack: one column, both halves full width, primary on top (Joshua
 * 2026-08-16, score screen). Distinct from --solo, which is one FULL-WIDTH
 * control; this is two stacked. The score screen earns the exception
 * because Start is the entire purpose of the screen and the 2-up form gave
 * Back an equal footprint next to it. Order comes from the DOM (Start is
 * written first) rather than `order:`, so reading, tab and visual order
 * cannot drift apart. */
.stl-actionpair--stack { grid-template-columns: 1fr; }
/* A pair whose second half is conditionally hidden collapses to one
 * column rather than leaving back marooned at 50% beside a gap. Results
 * is the case: writeStats hides "Run the Signal" once there is nothing
 * left to drill, and back has to survive that on its own. */
.stl-actionpair:has(> [hidden]) { grid-template-columns: 1fr; }
/* Children own the cell, not their own width/margin — the grid is the
 * single spacing authority. .stl-radar__cta carries width:100% plus a
 * margin-top for the un-paired layout; both have to go here.
 * The .stl-radar__cta selector is doubled up because `.stl-assess
 * .stl-radar__cta` (0,2,0) sets its own margin-top ~10k lines below
 * this rule, and at equal specificity the later rule wins — that margin
 * pushed the grid row 20px taller than the CTA, so the stretched back
 * button rendered 67px against the CTA's 47px. */
.stl-actionpair > .stl-btn { width: 100%; margin-top: 0; }
.stl-actionpair > .stl-btn.stl-radar__cta { margin-top: 0; }

/* Doubled class (0,2,0) on purpose: the `.stl-btn` base block lives at
 * ~line 4370, well after this one, so a single `.stl-btn--back` (0,1,0)
 * loses every shared property to it on source order — padding and
 * font-size silently fell back to the base 9px/13.5px and the pair
 * stopped matching the CTA's box. */
.stl-btn.stl-btn--back {
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
  color: var(--stl-text-muted);
  font-weight: 500;
  /* Match the hero CTA's box exactly (see .stl-radar__cta.stl-btn--pill-rainbow). */
  padding: 13px 18px;
  font-size: 14px;
  border-radius: var(--r-pill);
  gap: 6px;
  min-width: 0;
  box-shadow: none;
}
.stl-btn.stl-btn--back:hover {
  color: var(--stl-text);
  border-color: var(--stl-accent-line);
  background: var(--stl-accent-soft);
  transform: none;
}
.stl-btn.stl-btn--back svg {
  flex: none;
  width: 15px; height: 15px;
  transition: transform var(--dur-fast) var(--ease-spring);
}
.stl-btn.stl-btn--back:hover svg { transform: translateX(-2px); }
/* The label is written by JS and varies with the resolved parent, so
 * it has to survive a narrow cell without wrapping the button taller
 * than its rainbow twin. */
.stl-btn.stl-btn--back .stl-btn__lbl {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
/* Compact variant — the signal screen's flow header, where the exit has
 * to sit beside a progress readout rather than in an action row. */
.stl-btn.stl-btn--back-sm {
  padding: 7px 13px;
  font-size: 12.5px;
}
.stl-btn.stl-btn--back-sm svg { width: 13px; height: 13px; }

/* The second sanctioned form: the CRUMB. Used only for stepping UP a
 * sub-view inside a screen (learn: practice → lesson → index), where a
 * top-left crumb is the honest affordance and an action-row button
 * would read as "leave this screen". Screen-level back always takes the
 * action-row form above. Two forms, one rule — down from six positions
 * and six weights before this. */
.stl-backcrumb {
  appearance: none; -webkit-appearance: none;
  background: transparent; border: 0; padding: 0;
  font: inherit; font-size: 13px; font-weight: 500;
  color: var(--stl-text-muted);
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  justify-self: start;
  margin-bottom: var(--s-4);
  border-radius: var(--r-sm);
  transition: color var(--dur-base) ease;
}
.stl-backcrumb svg {
  width: 14px; height: 14px; flex: none;
  transition: transform var(--dur-fast) var(--ease-spring);
}
.stl-backcrumb:hover { color: var(--stl-text); }
.stl-backcrumb:hover svg { transform: translateX(-2px); }
.stl-backcrumb:focus-visible { outline: 2px solid var(--stl-accent); outline-offset: 4px; }
.stl-backcrumb[hidden] { display: none; }

@media (max-width: 640px) {
  .stl-btn.stl-btn--back { padding: 13px 12px; font-size: 13px; }
  .stl-actionpair { gap: 6px; }
}

.stl-score__subjects-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.stl-subject-pill {
  appearance: none; -webkit-appearance: none;
  font: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px 7px 11px;
  border-radius: var(--r-pill);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  color: var(--stl-text-subtle);
  font-size: 13px;
  font-weight: 500;
  transition: border-color var(--dur-base) var(--ease-out),
              background   var(--dur-base) var(--ease-out),
              color        var(--dur-base) var(--ease-out);
}
.stl-subject-pill:hover {
  border-color: var(--stl-accent-line);
  color: var(--stl-text);
}
.stl-subject-pill__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: inherit;
  transition: color var(--dur-base) var(--ease-out);
}
.stl-subject-pill__icon svg { width: 100%; height: 100%; display: block; }
.stl-subject-pill[aria-pressed="true"] {
  border-color: var(--stl-accent);
  background: var(--stl-accent-soft);
  color: var(--stl-text);
}
.stl-subject-pill[aria-pressed="true"] .stl-subject-pill__icon {
  color: var(--stl-accent);
  filter: drop-shadow(0 0 4px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.45));
}
.stl-subject-pill:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
.stl-tier {
  /* native button reset */
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  cursor: pointer;
  text-align: left;
  /* tile chrome */
  display: grid;
  gap: 4px;
  padding: 14px 16px;
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  color: var(--stl-text-muted);
  position: relative;
  overflow: hidden;
  transition: border-color var(--dur-base) var(--ease-spring),
              background   var(--dur-base) var(--ease-spring),
              color        var(--dur-base) var(--ease-spring),
              box-shadow   var(--dur-base) var(--ease-spring),
              transform    var(--dur-fast) var(--ease-spring);
}
.stl-tier__name {
  font-family: var(--stl-font-display);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--stl-text);
}
.stl-tier__range {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--stl-text-subtle);
}
/* Composer difficulty tiers carry no range subtitle (the teacher just
 * toggles tiers), so center the single label instead of top-left-
 * aligning it like the score screen's two-line pills. The composer
 * form column is narrow (teacher shell is 880px max, split 1.4:1),
 * so a 4-up row squeezed tiles narrower than their labels and
 * clipped "Medium"/"Insane" off-center — lay them 2×2 instead. */
#composer-tiers {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
#composer-tiers .stl-tier {
  align-content: center;
  justify-items: center;
  text-align: center;
  min-height: 56px;
  padding: 12px 10px;
}
/* Mobile: the generic ≤640px rule below turns .stl-tier into a tiny
 * inline pill (baseline-aligned, 6px padding) which, combined with the
 * 56px min-height above, rendered the composer tiers as squished ovals
 * with the label overflowing the button. Keep the composer tiers as a
 * clean 2×2 grid of centered tiles instead. */
@media (max-width: 640px) {
  #composer-tiers {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
  }
  #composer-tiers .stl-tier {
    display: grid;
    gap: 2px;
    min-height: 48px;
    padding: 10px 12px;
    border-radius: var(--r-md);
    background: var(--stl-surface);
  }
  #composer-tiers .stl-tier__name { font-size: 14px; font-family: var(--stl-font-display); font-weight: 600; }
  #composer-tiers .stl-tier__range { font-size: 10.5px; }
}
.stl-tier:hover {
  transform: translateY(-1px);
  border-color: var(--stl-accent-line);
  background: var(--stl-surface-2);
  box-shadow: 0 6px 16px -10px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.4);
}
.stl-tier:active { transform: translateY(0); }
.stl-tier:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
@media (hover: none) {
  .stl-tier:hover { transform: none; box-shadow: none; }
}
/* Active = the tier the current score sits in. Lavender border +
 * subtle accent fill so the user can see at a glance which band
 * their target lands in. */
.stl-tier.is-active {
  border-color: var(--stl-accent);
  background: var(--stl-accent-soft);
}
.stl-tier.is-active .stl-tier__name { color: var(--stl-text); }
.stl-tier.is-active:hover {
  border-color: var(--stl-accent);
  background: var(--stl-accent-soft);
}

/* NOTE ON POSITION: this block lives HERE, after the generic .stl-tier
 * rules, and not up with the other score-screen layout. It was written
 * there first and lost every property it shares with `.stl-tier` and
 * `.stl-tier.is-active` on source order at equal specificity — the
 * selected tile came out wearing the theme accent (green) instead of the
 * neutral it asks for. Same trap as the two `.stl-btn--back` and
 * `.stl-radar__cta` notes elsewhere in this file. Moving it down fixes
 * desktop; the mobile score-screen override needs its own rule because
 * `[data-screen="score"] .stl-tier.is-active` outranks it on specificity.
 */
/* ---- Tailored: "auto adjusted" difficulty (Joshua 2026-08-16) --------
 * Spans the full row UNDER the four buckets rather than sitting in them
 * as a fifth column. That is the whole layout argument: Tailored is not
 * a peer of Easy/Medium/Hard/Insane, it is an alternative to choosing at
 * all, and a fifth equal column would have said otherwise (as well as
 * squeezing four legible tiles into five cramped ones).
 *
 * It wears NO tier colour. Easy/Medium/Hard/Insane own green/yellow/red/
 * accent, and borrowing any of them would imply a fixed difficulty for a
 * control whose entire point is that it moves. The signal trio carries
 * the meaning instead: it spans the range. */
.stl-tier--tailored {
  grid-column: 1 / -1;
  flex-direction: row;
  align-items: center;
  gap: var(--s-2);
  text-align: left;
}
.stl-tier--tailored .stl-tier__range {
  /* The subtext sits beside the name, not under it, so the full-width
   * tile stays one line tall instead of towering over the four above. */
  margin: 0;
  font-family: var(--stl-font-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
/* ACTIVE: the rainbow gradient treatment (Joshua 2026-08-17).
 *
 * Same device as the first-timer lead card and the hero CTA — a border-box
 * gradient border over a padding-box fill, plus a masked --stl-rainbow-soft
 * wash inside. Under the signal theme --stl-rainbow IS the red→yellow→green
 * ramp, which is why it is the right treatment for this tile specifically:
 * Tailored spans the whole range, so it wears the whole range.
 *
 * It replaces the signal dots, which said the same thing in three 9px
 * circles and were the widest element in a 133px tile once Tailored moved
 * into the row.
 *
 * WHAT THE OLD COMMENT HERE WARNED ABOUT, and why the gradient is not the
 * same mistake: the first version of this active state used --stl-accent,
 * which under the signal theme paints GREEN — so a selected Tailored tile
 * sat beside a green "Easy" pill wearing the same green and read as a
 * difficulty. The rainbow cannot be confused for a tier because no tier
 * wears more than one hue. That was always the argument for the dots; it
 * transfers intact.
 *
 * Border width stays 1px, matching the inactive state, so selecting the
 * tile does not reflow the row. (1.5px, as the lead card uses, shifted
 * every tile beside it by half a pixel and the whole row jittered on
 * toggle.) */
/* The padding-box fill MUST be opaque. The first version used
 * --stl-surface-2, which is rgba(255,255,255,0.055) — so the
 * full-saturation border-box rainbow underneath showed straight through and
 * painted the entire tile at near-full chroma, drowning "Auto adjusted". Both
 * existing users of this device (.stl-assess-entry--lead, .stl-btn--pill-
 * rainbow) use an opaque colour, and that is why. --stl-bg is theme-aware, so
 * this stays correct in light mode where a hardcoded hex would not. */
.stl-tier--tailored.is-active {
  border-color: transparent;
  background:
    linear-gradient(var(--stl-bg), var(--stl-bg)) padding-box,
    var(--stl-rainbow) border-box;
}
.stl-tier--tailored.is-active::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--stl-rainbow-soft);
  /* Fades out across the tile so the label side stays legible rather than
   * sitting on full chroma — same mask as .stl-assess-entry--lead::before. */
  -webkit-mask-image: linear-gradient(135deg, #000 0%, rgba(0, 0, 0, 0.62) 55%, transparent 100%);
  mask-image: linear-gradient(135deg, #000 0%, rgba(0, 0, 0, 0.62) 55%, transparent 100%);
  pointer-events: none;
}
.stl-tier--tailored.is-active .stl-tier__name { color: var(--stl-text); }
.stl-tier--tailored.is-active .stl-tier__range { color: var(--stl-text-muted); }
/* The name and range sit above the wash. The base tile is a grid with no
 * z-index anywhere, so without this the ::before paints over them. */
.stl-tier--tailored.is-active .stl-tier__name,
.stl-tier--tailored.is-active .stl-tier__range { position: relative; }
/* Disabled tier — no live questions for this test/subject yet. Reads as
   inert (greyed, no hover lift, not-allowed cursor) so the empty tier
   (e.g. SAT-math Insane) is visibly unavailable rather than producing an
   empty test. */
.stl-tier:disabled,
.stl-tier[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
  background: transparent;
}
.stl-tier:disabled:hover,
.stl-tier[aria-disabled="true"]:hover {
  transform: none;
  box-shadow: none;
  border-color: var(--stl-hairline);
}

/* Mobile collapse: row of pills with the tier name + range inline. */
@media (max-width: 640px) {
  .stl-score__tiers {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  .stl-tier {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 6px 12px;
    border-radius: var(--r-pill);
    background: transparent;
  }
  .stl-tier__name { font-size: 12px; font-family: var(--stl-font-body); font-weight: 500; }
  .stl-tier__range { font-size: 11px; }
  /* The container is flex here, not grid, so `grid-column: 1 / -1` does
   * nothing — Tailored needs flex-basis to keep its own row. Without this
   * it wraps in beside a bucket pill and reads as a fifth difficulty. */
  .stl-tier--tailored {
    flex: 1 0 100%;
    justify-content: flex-start;
    align-items: center;
    padding: 8px 12px;
  }
  .stl-tier--tailored .stl-tier__range { font-size: 9px; }
}

/* --- quiz top bar ----------------------------------------------------- */
.stl-quiz__top { display: grid; gap: var(--s-3); }
.stl-quiz__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}
.stl-quiz__counter {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--stl-text-muted);
}
/* Signal-segmented progress — one tick per question. Ticks start as
 * faint track marks, the current question carries an accent underline,
 * and each commit paints its tick in the chosen signal color (gray for
 * skip). The bar doubles as a live confidence map of the whole test. */
.stl-quiz__progress {
  display: flex;
  gap: 2px;
  height: 3px;
}
.stl-quiz__seg {
  flex: 1;
  min-width: 1px;
  border-radius: 2px;
  background: var(--stl-surface-2);
  transition: background 240ms var(--ease-out);
}
.stl-quiz__seg.is-current { background: var(--stl-accent-line); }
.stl-quiz__seg.is-green   { background: var(--stl-green); }
.stl-quiz__seg.is-yellow  { background: var(--stl-yellow); }
.stl-quiz__seg.is-red     { background: var(--stl-red); }
.stl-quiz__seg.is-skip    { background: var(--stl-hairline-hi); }
/* 60+ question tests (full-length ACT/HSPT): tighter gaps, square ticks. */
.stl-quiz__progress--dense { gap: 1px; }
.stl-quiz__progress--dense .stl-quiz__seg { border-radius: 1px; }

/* Bluebook-style math tools (Desmos calculator, reference sheet later) */
.stl-quiz__tools {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-left: auto;
}
@media (max-width: 640px) {
  .stl-quiz__tools { display: none; }
}
.stl-quiz-tool {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 4px 12px;
  border-radius: var(--r-pill);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  color: var(--stl-text);
  font-family: var(--stl-font-body);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: border-color 160ms var(--ease-out), background 160ms var(--ease-out);
}
.stl-quiz-tool:hover {
  border-color: var(--stl-accent-line);
  background: var(--stl-surface-2);
}
.stl-quiz-tool:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
.stl-quiz-tool__icon {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--stl-accent);
}
.stl-mobile-bar__accessory--tools {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
@media (min-width: 641px) {
  #btn-quiz-calculator-mobile { display: none !important; }
}

/* Desmos calculator panel (dialog) */
.stl-desmos-panel {
  /* Non-modal side dock (Mike item 14b): the panel opens pinned to the
   * right so the problem stays visible beside it. Draggable by the head
   * (JS sets left/top) and resizable from the corner. */
  position: fixed;
  inset: auto;
  right: 16px;
  top: 64px;
  /* Taller + a touch wider (Mike 2026-07-25, raised again same day):
   * when the Desmos on-screen keypad drops down it takes ~280px of
   * the instance — at the old 560px height it buried the expression
   * list, so you couldn't see the equation you were typing. 780px
   * (viewport-capped) keeps the expression rows clearly visible above
   * an open keypad in EVERY section: tests, lessons, Run the Signal. */
  /* Wider still (Mike 2026-07-26): at 480px Desmos squeezed the graph
   * into a thin zoomed strip on the right. ~680px lets the expression
   * list and the graph each take roughly half the panel. */
  width: min(680px, calc(100vw - 48px));
  max-width: none;
  height: min(780px, calc(100vh - 96px));
  max-height: calc(100vh - 32px);
  min-width: 320px;
  min-height: 300px;
  margin: 0;
  padding: 0;
  resize: both;
  overflow: hidden;
  z-index: 90;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  /* Solid themed surface (Mike bug 2026-07-25): --stl-surface-1 is
   * UNDEFINED and resolved to transparent, so the panel chrome (head +
   * any gap around the Desmos iframe) showed the page through it — on
   * Run the Signal you could read "3 of 10" behind the calculator.
   * Same fix the reference sheet already carries. */
  background: var(--stl-bg-2);
  color: var(--stl-text);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
}
.stl-desmos-panel[open] {
  display: flex;
  flex-direction: column;
}
/* Only paints if a legacy engine fell back to showModal(). */
.stl-desmos-panel::backdrop {
  background: rgba(5, 5, 9, 0.35);
}
.stl-desmos-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--stl-hairline);
  cursor: move;
  user-select: none;
  touch-action: none;
  flex: none;
}
.stl-desmos-panel__title {
  margin: 0;
  font-family: var(--stl-font-body);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.stl-desmos-panel__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--stl-text-muted);
  cursor: pointer;
}
.stl-desmos-panel__close:hover { color: var(--stl-text); border-color: var(--stl-hairline-hi); }
.stl-desmos-panel__body {
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
}
@media (max-width: 640px) {
  /* Full-screen sheet on phones — no room for side-by-side there. */
  .stl-desmos-panel {
    top: 0;
    right: 0;
    left: 0;
    width: 100vw;
    max-height: 100vh;
    height: 100vh;
    min-width: 0;
    margin: 0;
    border-radius: 0;
    resize: none;
  }
  .stl-desmos-panel__head { cursor: default; }
  .stl-desmos-panel__body {
    min-height: 280px;
  }
}

/* Harden the quiz-tools hide: the author `display:inline-flex` on the
 * container and buttons wins over the UA `[hidden]{display:none}` (author
 * origin beats UA regardless of specificity), so the hidden attr alone
 * wouldn't collapse them on non-math questions. Force it. Covers the
 * calculator (existing) and the reference button (FR-1, new). */
.stl-quiz__tools[hidden] { display: none !important; }
.stl-quiz-tool[hidden] { display: none !important; }

/* ---- SAT/PSAT math reference sheet (dialog) — FR-1 Bluebook parity ----
 * Modal overlay you glance at and close. Our own markup (KaTeX + inline
 * SVG), themed to tokens so it renders crisply in light/dark. Free on
 * every SAT/PSAT math question — never gated. */
.stl-refsheet {
  /* Docked NON-MODAL side panel (Mike 2026-07-26, Bluebook parity;
   * moved to the RIGHT 2026-07-30 per Mike — both tools now dock on
   * the same side and the problem always slides LEFT, so opening
   * either tool feels identical). Draggable by the head (JS) +
   * resizable from the corner, same treatment as the Desmos panel.
   * If BOTH are open the later one sits on top; either can be dragged
   * aside. */
  position: fixed;
  inset: auto;
  right: 16px;
  top: 64px;
  width: min(640px, calc(100vw - 48px));
  max-width: none;
  height: min(780px, calc(100vh - 96px));
  max-height: calc(100vh - 32px);
  min-width: 320px;
  min-height: 260px;
  margin: 0;
  padding: 0;
  resize: both;
  z-index: 90;
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-lg);
  /* Solid, defined, themed surface — NOT the undefined --stl-surface-1 the
   * Desmos panel leaned on (that resolves to transparent). The reference sheet
   * carries text/formulas, so it needs an opaque readable background. */
  background: var(--stl-bg-2);
  color: var(--stl-text);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
.stl-refsheet[open] { display: flex; flex-direction: column; }
/* Only paints if a legacy engine fell back to showModal(). */
.stl-refsheet::backdrop { background: rgba(5, 5, 9, 0.35); }

/* ── Bluebook-style content slide (Mike 2026-07-26; unified RIGHT dock
 * 2026-07-30) ─────────────────────────────────────────────────────────
 * When a tool docks, the QUESTION slides out from under it: body
 * padding reserves the dock's strip on the RIGHT, and .stl-main
 * re-centers in the remaining space. BOTH tools dock right now, so
 * the slide direction is always the same; with both open the single
 * strip is sized for the wider panel (the calculator's rule is
 * declared last, so it wins when both classes are present). Phones
 * show the panels full-screen instead. */
@media (min-width: 981px) {
  body { transition: padding 0.28s ease; }
  html.stl-ref-open body { padding-right: calc(min(680px, 48vw) + 24px); }
  html.stl-calc-open body { padding-right: calc(min(680px, 48vw) + 24px); }
  /* ...but NOT the navbar. The reservation is padding on <body>, and the
   * header is a child of body, so it was getting squeezed along with the
   * question — at 1440px the bar shrank from 1440 to 736 and the nav
   * links piled on top of each other (Josh 2026-08-11: "it caused the
   * navbar to squish (even though the calculator is supposed to be
   * floating on top)"). The header is chrome, not content: it should
   * stay full-bleed under the floating panel. A negative right margin
   * exactly equal to the reservation walks it back out to the padding
   * edge — it never crosses it, so no horizontal overflow. The matching
   * transition keeps it from snapping while the content slides. */
  .stl-header { transition: margin-right 0.28s ease; }
  html.stl-ref-open  .stl-header { margin-right: calc(-1 * (min(680px, 48vw) + 24px)); }
  html.stl-calc-open .stl-header { margin-right: calc(-1 * (min(680px, 48vw) + 24px)); }
  /* Panels cap at the same vw the strip reserves, so panel and strip
   * always agree and the question is never covered. */
  .stl-refsheet { width: min(640px, 46vw); }
  .stl-desmos-panel { width: min(680px, 48vw); }
  /* Both open: nudge the sheet so the calculator's head stays visible. */
  html.stl-calc-open .stl-refsheet[open] { top: 96px; right: 32px; }
}
@media (max-width: 640px) {
  /* Full-screen sheet on phones — no room for side-by-side there. */
  .stl-refsheet {
    top: 0; left: 0; right: 0;
    width: 100vw; height: 100vh;
    max-height: 100vh; min-width: 0;
    border-radius: 0; resize: none;
  }
}
.stl-refsheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--stl-hairline);
  flex: none;
}
/* The clean pre-rendered sheet (Mike 2026-07-25): white SVG artwork
 * displayed verbatim — rounded, full width, crisp at any size. */
.stl-refsheet__img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 10px;
  background: #fff;
}
.stl-refsheet__title {
  margin: 0;
  font-family: var(--stl-font-body);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.stl-refsheet__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--stl-text-muted);
  cursor: pointer;
  transition: color 160ms var(--ease-out), border-color 160ms var(--ease-out);
}
.stl-refsheet__close:hover { color: var(--stl-text); border-color: var(--stl-hairline-hi); }
.stl-refsheet__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--s-4);
}
.stl-refsheet__note {
  margin: 0 0 var(--s-3);
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-refsheet__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(164px, 1fr));
  gap: var(--s-3);
}
.stl-refsheet__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  padding: var(--s-3);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  background: var(--stl-surface-2);
  text-align: center;
}
.stl-refsheet__fig {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 88px;
  width: 100%;
  color: var(--stl-text-muted);
}
.stl-refsheet__fig svg { max-height: 88px; width: auto; overflow: visible; }
.stl-refsheet__fig text {
  fill: var(--stl-text-muted);
  font-family: var(--stl-font-mono);
  font-size: 11px;
  font-style: italic;
}
.stl-refsheet__formula {
  font-size: 15px;
  line-height: 1.5;
  color: var(--stl-text);
}
.stl-refsheet__formula .katex { font-size: 1.05em; }
.stl-refsheet__facts {
  margin-top: var(--s-4);
  padding-top: var(--s-4);
  border-top: 1px solid var(--stl-hairline);
  display: grid;
  gap: 9px;
}
.stl-refsheet__fact {
  display: flex;
  gap: 9px;
  align-items: baseline;
  font-size: 13px;
  line-height: 1.5;
  color: var(--stl-text-muted);
}
.stl-refsheet__fact::before {
  content: "";
  flex: none;
  width: 5px;
  height: 5px;
  margin-top: 7px;
  border-radius: 50%;
  background: var(--stl-accent);
}
@media (max-width: 640px) {
  .stl-refsheet {
    width: 100vw;
    max-width: 100vw;
    max-height: 100vh;
    height: 100dvh;
    border-radius: 0;
    border: 0;
  }
  .stl-refsheet__grid { grid-template-columns: repeat(auto-fill, minmax(146px, 1fr)); }
}

/* Live-quiz step nav — back · skip/next (Joshua 2026-08-16).
 *
 * One axis, one cluster, centred under the signal buttons: chevron-left
 * "Back", a hairline, then the forward control with a chevron-right. The
 * hairline is what makes the pair read as a single navigation control
 * rather than two unrelated links; without it "Back" floats and looks
 * like a stray.
 *
 * These are deliberately QUIETER than the signal buttons — subtle text,
 * no fill, no border. The commit controls have to stay the loudest thing
 * in the footer, because choosing a signal is the point of the screen and
 * navigating away from it is the exception.
 *
 * HISTORY: this pair previously lived beside the question counter, on the
 * theory that navigation belongs with position and that putting it in the
 * footer would shift the signal buttons whenever Back appeared on question
 * 2. That second concern was real, and is solved here by keeping Back
 * permanently mounted and merely disabled on question 1, so the row's
 * height and width never change. The first concern lost to a simpler
 * truth: nobody found it up there. */
.stl-stepnav {
  justify-self: center;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-1);
}
/* No per-element [hidden] guard here any more: the blanket
 * `.stl-app [hidden] { display: none !important }` near the top of this
 * file covers it. This is the rule that used to live here, and it is why
 * that blanket rule exists — Skip and Next were both visible on question 1
 * because `display: inline-flex` below beat the UA's [hidden]. */
.stl-stepnav__rule {
  width: 1px;
  height: 15px;
  background: var(--stl-hairline-hi);
  flex: 0 0 auto;
}
.stl-stepnav__btn {
  appearance: none; -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: var(--s-2) var(--s-2);
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  font: inherit;
  font-size: 0.85rem;
  line-height: 1.2;
  color: var(--stl-text-subtle);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-base) var(--ease-out),
              opacity var(--dur-base) var(--ease-out);
}
/* The label carries the underline, not the button — otherwise the rule
 * runs under the chevron too and the arrow looks struck through. */
.stl-stepnav__btn > span {
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--stl-hairline-hi);
  transition: text-decoration-color var(--dur-base) var(--ease-out);
}
.stl-stepnav__chev {
  flex: 0 0 auto;
  opacity: 0.75;
  transition: transform var(--dur-base) var(--ease-spring),
              opacity var(--dur-base) var(--ease-out);
}
.stl-stepnav__btn:hover:not(:disabled) { color: var(--stl-text); }
.stl-stepnav__btn:hover:not(:disabled) > span { text-decoration-color: var(--stl-text-muted); }
.stl-stepnav__btn:hover:not(:disabled) .stl-stepnav__chev { opacity: 1; }
/* The chevron nudges the way the button travels. Two pixels — enough to
 * say "this moves you", not enough to be a performance. */
.stl-stepnav__btn--back:hover:not(:disabled) .stl-stepnav__chev { transform: translateX(-2px); }
.stl-stepnav__btn--fwd:hover:not(:disabled) .stl-stepnav__chev { transform: translateX(2px); }
.stl-stepnav__btn:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
/* Question 1: present but inert, so the row keeps its shape and the
 * student learns the control exists before they need it. */
.stl-stepnav__btn:disabled {
  opacity: 0.38;
  cursor: default;
}
.stl-stepnav__btn:disabled > span { text-decoration-color: transparent; }
@media (prefers-reduced-motion: reduce) {
  .stl-stepnav__chev { transition: opacity var(--dur-base) var(--ease-out); }
  .stl-stepnav__btn--back:hover:not(:disabled) .stl-stepnav__chev,
  .stl-stepnav__btn--fwd:hover:not(:disabled) .stl-stepnav__chev { transform: none; }
}

/* pills — semantic but quiet */
.stl-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  background: transparent;
  border: 1px solid var(--stl-hairline);
  color: var(--stl-text-muted);
  font-weight: 500;
}
.stl-pill::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--stl-text-faint);
}
.stl-pill[data-d="easy"]   { color: var(--stl-green-text); border-color: rgba(var(--stl-green-rgb), 0.28); }
.stl-pill[data-d="easy"]::before   { background: var(--stl-green);  box-shadow: 0 0 6px var(--stl-green); }
.stl-pill[data-d="medium"] { color: var(--stl-yellow-text); border-color: rgba(var(--stl-yellow-rgb), 0.28); }
.stl-pill[data-d="medium"]::before { background: var(--stl-yellow); box-shadow: 0 0 6px var(--stl-yellow); }
.stl-pill[data-d="hard"]   { color: var(--stl-red-text); border-color: rgba(var(--stl-red-rgb), 0.32); }
.stl-pill[data-d="hard"]::before   { background: var(--stl-red); box-shadow: 0 0 6px var(--stl-red); }
.stl-pill[data-d="insane"] { color: var(--stl-blue-text); border-color: rgba(var(--stl-blue-rgb), 0.32); }
.stl-pill[data-d="insane"]::before { background: var(--stl-blue);    box-shadow: 0 0 6px var(--stl-blue); }

/* Light-mode difficulty pills — the pale 200-tier text colors above
 * (#fde68a etc.) are tuned for the dark canvas and disappear on
 * white. Yellow is the worst offender (lightest hue) but all four
 * need bumping down to ~700-tier so they actually read. Borders also
 * darkened slightly so the pill chrome doesn't ghost out. The dots
 * stay bright — they're the color signal, not the text. */
:root[data-theme="light"] .stl-pill[data-d="easy"]   { color: #15803d; border-color: rgba(var(--stl-green-rgb), 0.45); }
:root[data-theme="light"] .stl-pill[data-d="medium"] { color: #a16207; border-color: rgba(202, 138, 4, 0.50); }
:root[data-theme="light"] .stl-pill[data-d="hard"]   { color: #b91c1c; border-color: rgba(var(--stl-red-rgb), 0.50); }
:root[data-theme="light"] .stl-pill[data-d="insane"] { color: #1d4ed8; border-color: rgba(37, 99, 235, 0.50); }

/* Static elapsed-time pill — used on the Results screen for the
 * frozen total-time + per-question pacing summary. Same .stl-pill
 * chrome as the live timer but no pulse animation (test is over).
 * Number sits brighter (var(--stl-text)) so the value pops against
 * the muted unit label. */
.stl-results__pacing {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 8px;
}
.stl-pill--time {
  color: var(--stl-text-muted);
  border-color: rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.32);
  gap: 8px;
}
.stl-pill--time::before {
  background: rgb(var(--stl-tint, var(--stl-rainbow-mid-rgb)));
  box-shadow: 0 0 6px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.45);
}
.stl-pill__num {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--stl-text);
  letter-spacing: 0;
  text-transform: none;
  font-size: 11.5px;
}
.stl-pill__unit {
  color: var(--stl-text-subtle);
}

/* Live elapsed-time pill — sits inline with the question meta row.
 * Dot pulses subtly to telegraph "this is live, ticking". Color
 * pulls from --stl-tint so the timer wears the active test's color
 * (orange ACT, green ISEE, etc.) — same identity logic as the
 * difficulty pills inherit per-test. Number text is mono with
 * tabular-nums so digits don't shift width as the timer ticks. */
.stl-pill--timer {
  color: var(--stl-text);
  border-color: rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.32);
}
.stl-pill--timer::before {
  background: rgb(var(--stl-tint, var(--stl-rainbow-mid-rgb)));
  box-shadow: 0 0 6px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.55);
  animation: stl-timer-pulse 2s ease-in-out infinite;
}
.stl-pill--timer #quiz-timer-text {
  font-variant-numeric: tabular-nums;
}
@keyframes stl-timer-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}
@media (prefers-reduced-motion: reduce) {
  .stl-pill--timer::before { animation: none; opacity: 0.85; }
}

/* Signal-state chips ("Green · Correct" … "Red · Incorrect"), used by
 * Run the Signal cards and the review sheet. Coloring rule (Mike
 * 2026-07-26): the chip is tinted by the SIGNAL COLOR it names — the
 * first word is a color, so the chip must be that color (the old
 * outcome-based tints made "Green · Incorrect" render red and left
 * "Red · Incorrect" gray). Correct = outline only; Incorrect = same
 * color with a soft filled background so needs-attention states pop
 * without contradicting the label. Chips never wrap. */
.stl-pill[data-result] { white-space: nowrap; }
.stl-pill[data-result="mastery"] {
  color: var(--stl-green-text);
  border-color: rgba(var(--stl-green-rgb), 0.45);
}
.stl-pill[data-result="mastery"]::before {
  background: var(--stl-green);
  box-shadow: 0 0 6px var(--stl-green);
}
.stl-pill[data-result="misconception"] {
  color: var(--stl-green-text);
  border-color: rgba(var(--stl-green-rgb), 0.7);
  background: rgba(var(--stl-green-rgb), 0.12);
}
.stl-pill[data-result="misconception"]::before {
  background: var(--stl-green);
  box-shadow: 0 0 6px var(--stl-green);
}
.stl-pill[data-result="lucky"] {
  color: var(--stl-yellow-text);
  border-color: rgba(var(--stl-yellow-rgb), 0.45);
}
.stl-pill[data-result="lucky"]::before {
  background: var(--stl-yellow);
  box-shadow: 0 0 6px var(--stl-yellow);
}
.stl-pill[data-result="gap"] {
  color: var(--stl-yellow-text);
  border-color: rgba(var(--stl-yellow-rgb), 0.7);
  background: rgba(var(--stl-yellow-rgb), 0.12);
}
.stl-pill[data-result="gap"]::before {
  background: var(--stl-yellow);
  box-shadow: 0 0 6px var(--stl-yellow);
}
.stl-pill[data-result="missed"],
.stl-pill[data-result="fluke"] {
  color: #fecaca;
  border-color: rgba(var(--stl-red-rgb), 0.45);
}
.stl-pill[data-result="missed"]::before,
.stl-pill[data-result="fluke"]::before {
  background: var(--stl-red);
  box-shadow: 0 0 6px var(--stl-red);
}
.stl-pill[data-result="cold"] {
  color: #fecaca;
  border-color: rgba(var(--stl-red-rgb), 0.7);
  background: rgba(var(--stl-red-rgb), 0.12);
}
.stl-pill[data-result="cold"]::before {
  background: var(--stl-red);
  box-shadow: 0 0 6px var(--stl-red);
}
/* The outcome WORD inside a signal chip keeps its own color (Mike
 * 2026-07-26): Correct is always green, Incorrect is always red —
 * regardless of the chip's signal tint around it. */
.stl-pill[data-result] .stl-pill__outcome--correct   { color: var(--stl-green); }
.stl-pill[data-result] .stl-pill__outcome--incorrect { color: var(--stl-red); }
/* Light-mode contrast for result pills. The dark-theme pastels above
 * (#bbf7d0 / #fde68a / #fecaca) wash out on white — same problem the
 * admin status badges had. Switch to the darker saturated status-text
 * tokens, which clear WCAG AA, and bump the border/background tint to
 * match. */
:root[data-theme="light"] .stl-pill[data-result="mastery"] {
  color: var(--stl-green-text);
  border-color: rgba(var(--stl-green-rgb), 0.55);
}
:root[data-theme="light"] .stl-pill[data-result="misconception"] {
  color: var(--stl-green-text);
  border-color: rgba(var(--stl-green-rgb), 0.7);
  background: rgba(var(--stl-green-rgb), 0.12);
}
:root[data-theme="light"] .stl-pill[data-result="lucky"] {
  color: var(--stl-yellow-text);
  border-color: rgba(202, 138, 4, 0.55);
}
:root[data-theme="light"] .stl-pill[data-result="gap"] {
  color: var(--stl-yellow-text);
  border-color: rgba(202, 138, 4, 0.7);
  background: rgba(202, 138, 4, 0.12);
}
:root[data-theme="light"] .stl-pill[data-result="missed"],
:root[data-theme="light"] .stl-pill[data-result="fluke"] {
  color: var(--stl-red-text);
  border-color: rgba(var(--stl-red-rgb), 0.55);
}
:root[data-theme="light"] .stl-pill[data-result="cold"] {
  color: var(--stl-red-text);
  border-color: rgba(var(--stl-red-rgb), 0.7);
  background: rgba(var(--stl-red-rgb), 0.12);
}
:root[data-theme="light"] .stl-pill[data-result] .stl-pill__outcome--correct   { color: var(--stl-green-text); }
:root[data-theme="light"] .stl-pill[data-result] .stl-pill__outcome--incorrect { color: var(--stl-red-text); }

/* --- quiz card -------------------------------------------------------- */
.stl-quiz__card { display: grid; gap: var(--gap-section); }
.stl-quiz__passage {
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--stl-text-muted);
  border-left: 2px solid var(--stl-hairline-hi);
  padding: var(--s-2) var(--s-4);
  white-space: pre-wrap;
}
.stl-quiz__stem {
  font-size: clamp(1.1rem, 2vw, 1.25rem);
  line-height: 1.45;
  margin: 0;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--stl-text);
}

/* Variable letters in stems / passages / explanations — italicized and
 * lavender-tinted so they read as math typography rather than narrative
 * text. The renderer (formatStemHtml in app.js) wraps standalone single
 * letters in this span at render time, so no per-question markup is
 * needed. SAT print typesets all variables italic; this matches that
 * convention while picking up the v2-aligned accent color so they pop. */
.stl-quiz__var {
  font-style: italic;
  color: var(--stl-accent);
  /* Slight reduction in font weight when bold-adjacent (e.g., next to
   * the stem's medium-weight body text) keeps the variable from looking
   * heavier than the surrounding text. */
  font-weight: 500;
}

/* Math expression run — produced by wrapExpressions() in app.js.
 * Wraps any contiguous run that contains a variable plus math chars
 * (operators, digits, parens) so the WHOLE expression inherits the
 * accent color. Without this, (x + 4)(x + 2) reads as purple x's and
 * parens but white digits — breaking the visual unit of the equation.
 * Inner .stl-quiz__var keeps its italic; the operators/digits/parens
 * pick up the color via inheritance from this span. */
.stl-quiz__expr {
  color: var(--stl-accent);
  /* Match weight to the surrounding stem text so the expression reads
   * as a unit at the same density (the variable's italic still sets
   * it apart inside the run). */
  font-weight: 500;
  /* Equations must never break across lines (Mike QA 2026-07-02: a stem
   * rendered "f(x) = (x+4)(x−" with the rest orphaned on the next line).
   * nowrap forbids internal breaks; inline-block makes the run a single
   * atomic box, so when it can't fit the remaining line width the WHOLE
   * equation drops to its own row. On viewports too narrow for the full
   * equation we accept light horizontal overflow over the alternative —
   * an equation broken in the middle of an operator. */
  white-space: nowrap;
  display: inline-block;
  max-width: 100%;
}

/* View-driven: Classic view adds stl-no-math-color to <body> (Modern
 * removes it — see applyView/applyMathColorPref in app.js), reverting both
 * variable letters and full expression runs to the surrounding text color.
 * Italic on variables is preserved — it's typesetting, not chroma. */
body.stl-no-math-color .stl-quiz__var,
body.stl-no-math-color .stl-quiz__expr {
  color: inherit;
}

/* KaTeX-rendered math segments ($…$ / $$…$$ in the bank). KaTeX output
 * inherits color, so tinting this wrapper tints the whole expression —
 * Modern view shows it in the accent (matching .stl-quiz__expr), Classic
 * leaves it the body color. Inline by default; $$…$$ becomes a centered
 * display block. */
.stl-katex { color: var(--stl-accent); }
body.stl-no-math-color .stl-katex { color: inherit; }
/* KaTeX splits inline math into several top-level chunks and lets the
 * browser line-break between them (after binary/relational operators) —
 * which is how "f(x) = (x+4)(x−" ended up cut mid-equation (Mike QA
 * 2026-07-02). Inline equations are atomic: nowrap forbids the internal
 * breaks and inline-block drops the WHOLE equation to its own row when
 * it can't fit the remaining line. Applies to stems, choices, and
 * explanations (formatRich is the single funnel). Display math is
 * already a block. */
.stl-katex:not(.stl-katex--display) {
  display: inline-block;
  white-space: nowrap;
  max-width: 100%;
}
.stl-katex--display { display: block; }
.stl-katex--display > .katex-display { margin: 0.5em 0; }
/* Stacked display equations (systems) sit tight — one gap, not two
 * (Mike, 2026-07-23: "noticeably large gap between the two equations"). */
.stl-katex--display + .stl-katex--display > .katex-display { margin-top: 0.15em; }
/* KaTeX's own error color shouldn't fight the accent tint. */
.stl-katex .katex-error { color: var(--stl-red) !important; }

/* ============== ADMIN: TESTS PANEL ==============
 * One row per test type with wordmark + name + question counts +
 * Active/Hidden switch. Same visual chrome as the existing question
 * rows so the admin feels like one cohesive surface. */
.stl-tests-panel {
  display: grid;
  gap: var(--s-3);
  margin-top: var(--s-3);
}
/* Global test-length options editor (top of the admin Tests panel). */
.stl-tests-globals {
  display: grid;
  gap: var(--s-1);
  padding: var(--s-3);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  background: var(--stl-surface);
}
.stl-tests-globals__head { display: grid; gap: 2px; }
.stl-tests-globals__row {
  display: flex;
  gap: var(--s-2);
  align-items: center;
}
.stl-tests-globals__row .stl-input { flex: 1; min-width: 0; }
.stl-btn--sm { padding: 6px 14px; font-size: 13px; }
/* Length picker on the create-test screen reuses the .stl-tier tiles. */
.stl-score__length-custom { margin-top: var(--s-2); }
.stl-score__length-custom[hidden] { display: none; }
.stl-tests-row {
  display: grid;
  gap: 0;
  padding: 0;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  background: var(--stl-surface);
  position: relative;
  overflow: hidden;
  /* Per-test hover: border + glow tint matches the user-facing
   * .stl-picker__tile so admin Tests rows feel like the same surface
   * users click in the create-test flow. No translate — keeps the
   * expanded subjects panel below from jumping with the row. */
  transition:
    opacity var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-spring),
    box-shadow var(--dur-base) var(--ease-spring);
}
.stl-tests-row:hover {
  border-color: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.55);
  box-shadow: 0 12px 28px -14px rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.45);
}
/* Hidden test — whole row dims to .55 (matching the subject-row
 * fade), so admins instantly see which tests are off. The toggle
 * itself stays at full opacity (overridden inside the row) so the
 * path back is obvious. */
.stl-tests-row[data-active="0"] .stl-tests-row__main > :not([data-test-toggle]) {
  opacity: 0.55;
  transition: opacity var(--dur-base) var(--ease-out);
}
.stl-tests-row::before {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(135deg, rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.06), transparent 60%);
  pointer-events: none;
}
/* Main row — wordmark / meta / counts / toggle. The whole row main
 * acts as a click target for collapse/expand (see initTestsPanel) —
 * buttons inside the row keep their own click handlers via
 * data-attribute matching, so they don't fall through to the
 * expand-zone. No separate chevron control; the cursor + background
 * hover lift signal the affordance. */
.stl-tests-row__main {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4);
  position: relative;
  transition: background var(--dur-base) var(--ease-out);
}
.stl-tests-row__main[data-test-expand-zone] { cursor: pointer; }
/* Click-zone hover background tints in the active test's accent so the
 * expand affordance reads in the test's own color instead of the old
 * uniform lavender. Pairs with the per-tint border + glow on the
 * surrounding .stl-tests-row:hover. */
.stl-tests-row__main[data-test-expand-zone]:hover {
  background: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.06);
}
.stl-tests-row__main[data-test-expand-zone]:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: -2px;
}
/* Subjects sub-section — indented under the parent test row. Each
 * row is one subject with a compact toggle. When the parent test is
 * Hidden (data-parent-active="0"), the whole block dims so admins
 * can see the subject state without it competing visually. */
.stl-tests-row__subjects {
  display: grid;
  gap: 0;
  padding: 0 var(--s-4) var(--s-3);
  margin-top: -4px;
  position: relative;
  border-top: 1px dashed var(--stl-hairline);
}
/* Author display:grid above wins over the user-agent [hidden] rule,
 * so we explicitly hide the collapsed state. Without this, toggling
 * the chevron rotated the icon but left the subjects block painted. */
.stl-tests-row__subjects[hidden] { display: none; }
.stl-tests-row__subjects[data-parent-active="0"] {
  opacity: 0.55;
}
.stl-tests-subject {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--s-3);
  padding: 9px 0 9px var(--s-5);
  border-bottom: 1px solid var(--stl-hairline);
  transition: opacity var(--dur-base) var(--ease-out);
}
.stl-tests-subject:last-child { border-bottom: 0; }
/* Disabled subject row — same .55 dim treatment used for the whole
 * block when the parent test is off. Keeps the visual hierarchy
 * consistent: anything not contributing to the live pool reads as
 * recessed. The toggle itself stays at full opacity so the user can
 * see/click the path back on. */
.stl-tests-subject[data-enabled="0"] .stl-tests-subject__meta {
  opacity: 0.55;
}
.stl-tests-subject__meta {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  min-width: 0;
}
.stl-tests-subject__name {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--stl-text);
}
.stl-tests-subject__count {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  color: var(--stl-text-subtle);
  letter-spacing: 0.02em;
}
/* Editable composition row — small numeric input + live-derived %
 * pill. Sits between the subject name and the live/total count so
 * the admin sees:  [name]  [12 q | 40%]  [85 live · 120 total]
 * The input width fits 3 digits; arrow buttons clipped on Firefox
 * via -moz-appearance, on Chrome via webkit-appearance. */
.stl-tests-subject__comp {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.stl-tests-subject__count-input {
  width: 52px;
  padding: 2px 6px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-sm);
  background: var(--stl-bg);
  color: var(--stl-text);
  font-family: var(--stl-font-mono);
  font-size: 12px;
  font-weight: 600;
  text-align: right;
  -moz-appearance: textfield;
  appearance: textfield;
}
/* Hide the spin buttons on WebKit/Blink — admin types or pastes the
 * number, the arrow controls steal horizontal space and feel chunky
 * in this dense row. */
.stl-tests-subject__count-input::-webkit-outer-spin-button,
.stl-tests-subject__count-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.stl-tests-subject__count-input:focus {
  outline: none;
  border-color: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.7);
  background: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.05);
}
.stl-tests-subject__comp-pct {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  font-weight: 500;
  color: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.85);
  letter-spacing: 0.02em;
  padding: 1px 7px;
  border-radius: var(--r-pill);
  background: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.10);
}
/* Disabled-subject dim — mirror the existing rule. */
.stl-tests-subject[data-enabled="0"] .stl-tests-subject__comp { opacity: 0.55; }

/* Theme color picker row inside the expanded test config. Swatch +
 * label + native <input type="color">, with an optional Reset link
 * when the current tint differs from the hardcoded default. Hides the
 * native picker UI behind the swatch — clicking the chip opens the
 * eyedropper without showing the OS color input itself. */
/* Labeled switches on each test row: Active + Coming soon
 * (2026-08-10). A 2×2 grid rather than two stacked flex rows: labels
 * right-align against a shared switch column so both toggles sit on
 * the same vertical line, and a hairline on the left separates the
 * control cluster from the count stats. display: contents lets the
 * label wrapper contribute its text + switch as siblings to the grid.
 * Coming soon reads amber when it's actually biting (test Active). */
.stl-tests-row__switches {
  display: grid;
  grid-template-columns: auto auto;
  align-items: center;
  column-gap: 12px;
  row-gap: 12px;
  flex: 0 0 auto;
  padding-left: 22px;
  margin-left: 8px;
  border-left: 1px solid var(--stl-hairline);
  align-self: center;
}
.stl-tests-row__swlabel { display: contents; }
.stl-tests-row__swtext {
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  white-space: nowrap;
  text-align: right;
  justify-self: end;
}
.stl-tests-row__switches .stl-switch { justify-self: start; }
.stl-tests-row[data-comingsoon="1"][data-active="1"] .stl-tests-row__swlabel--cs .stl-tests-row__swtext {
  color: var(--stl-yellow-text);
}
.stl-tests-row__cs-dot { color: var(--stl-yellow); font-size: 9px; vertical-align: 1px; }

/* ---- admin Metrics tab (2026-08-10): interest rollup cards ---- */
.stl-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 14px;
}
.stl-metrics__empty { grid-column: 1 / -1; display: grid; gap: 6px; padding: 28px 0; }
.stl-metrics__card {
  padding: 18px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background:
    linear-gradient(165deg, rgba(var(--tile-tint), 0.06) 0%, transparent 55%),
    var(--stl-surface);
}
.stl-metrics__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.stl-metrics__name { font-family: var(--stl-font-display); font-size: 16px; font-weight: 600; color: var(--stl-text); }
.stl-metrics__badge {
  font-family: var(--stl-font-mono);
  font-size: 10px; letter-spacing: 0.06em;
  padding: 3px 9px;
  border: 1px solid rgba(var(--stl-yellow-rgb), 0.45);
  border-radius: var(--r-pill);
  color: var(--stl-yellow-text);
  white-space: nowrap;
}
.stl-metrics__badge--off { border-color: var(--stl-hairline-hi); color: var(--stl-text-subtle); }
.stl-metrics__stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 0 0 14px; }
.stl-metrics__stats dt { font-size: 10.5px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--stl-text-subtle); margin-bottom: 2px; }
.stl-metrics__stats dd { margin: 0; font-family: var(--stl-font-display); font-size: 22px; font-weight: 650; color: var(--stl-text); font-variant-numeric: tabular-nums; }
.stl-metrics__spark { display: flex; align-items: flex-end; gap: 3px; height: 42px; }
.stl-metrics__spark i {
  flex: 1;
  min-height: 2px;
  border-radius: 2px 2px 0 0;
  background: rgb(var(--tile-tint));
  opacity: 0.85;
}
.stl-metrics__spark i[data-zero="1"] { background: var(--stl-surface-3); opacity: 1; height: 2px !important; }
/* Site-traffic card (2026-08-25): the PostHog rollup spans the grid and
   adds a four-up stat row + ranked screen list. Same card anatomy as the
   interest cards — tint flows through --tile-tint, everything tokens. */
.stl-metrics__card--wide { grid-column: 1 / -1; }
.stl-metrics__stats--four { grid-template-columns: repeat(4, 1fr); }
.stl-metrics__rows { display: grid; gap: 6px; margin-top: 14px; }
.stl-metrics__row {
  display: grid;
  grid-template-columns: minmax(72px, 140px) 1fr auto;
  align-items: center;
  gap: 10px;
}
.stl-metrics__row-label {
  font-family: var(--stl-font-mono);
  font-size: 11.5px;
  color: var(--stl-text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.stl-metrics__row-bar {
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--stl-surface-3);
  position: relative;
  overflow: hidden;
}
.stl-metrics__row-bar::after {
  content: "";
  position: absolute; inset: 0;
  width: var(--w, 0%);
  border-radius: inherit;
  background: rgba(var(--tile-tint), 0.75);
}
.stl-metrics__row-n {
  font-family: var(--stl-font-mono);
  font-size: 11.5px;
  color: var(--stl-text);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 640px) {
  .stl-metrics__stats--four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.stl-tests-row__color {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-2);
  padding-top: var(--s-3);
  border-top: 1px dashed var(--stl-hairline);
  font-size: 12px;
  color: var(--stl-text-muted);
}
.stl-tests-row__color-label {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  cursor: pointer;
  position: relative;
}
.stl-tests-row__color-swatch {
  display: inline-block;
  width: 22px; height: 22px;
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.25),
    inset 0 -1px 0 rgba(0, 0, 0, 0.20);
}
.stl-tests-row__color-text {
  font-weight: 500;
  color: var(--stl-text);
  letter-spacing: 0.01em;
}
/* Native color input — hide visually but stay clickable. Sized over
 * the swatch so a click on the chip opens the OS picker. */
.stl-tests-row__color-input {
  position: absolute;
  inset: 0;
  width: 22px; height: 22px;
  opacity: 0;
  cursor: pointer;
  /* Some browsers default to a 30x30 minimum — neutralize so the
   * hit area matches the visible swatch exactly. */
  border: 0;
  padding: 0;
  background: transparent;
}
.stl-tests-row__color-reset {
  margin-left: auto;
  font-size: 12px;
}

/* Reset-to-default link sits at the bottom of the expanded subjects
 * panel, only when at least one override is in play for this test. */
.stl-tests-row__reset {
  margin-top: var(--s-2);
  padding-top: var(--s-2);
  border-top: 1px dashed var(--stl-hairline);
  text-align: right;
  font-size: 12px;
}
.stl-tests-row__overridden-dot {
  color: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.9);
  font-size: 9px;
  vertical-align: middle;
}
/* Compact switch variant — about 70% the size of the default. Used
 * for subject child toggles so they read as subordinate to the
 * parent test toggle. Track + thumb sized so the thumb sits with
 * ~2px margins from each track edge in both states.
 *
 * The selectors here CHAIN .stl-switch.stl-switch--sm (specificity
 * 0,3,1) so they beat the base .stl-switch[aria-checked=...] rule
 * (specificity 0,2,1) which lives later in source order — without
 * the chain, the base rule won the cascade and slammed the small
 * thumb to a translateX(16) meant for the larger 38px track,
 * which made it bleed past the 32px small track's right edge. */
.stl-switch.stl-switch--sm { padding: 2px 4px; gap: 0; }
.stl-switch.stl-switch--sm .stl-switch__track { width: 32px; height: 18px; }
.stl-switch.stl-switch--sm .stl-switch__thumb { width: 12px; height: 12px; top: 2px; left: 2px; }
.stl-switch.stl-switch--sm[aria-checked="true"] .stl-switch__thumb { transform: translateX(14px); }

/* Subjects block uses the native [hidden] attr to collapse — no
 * height transition (a 0 → auto height tween needs JS to measure
 * heights, which adds layout thrash for marginal payoff here). The
 * cursor change on hover + the row-main background lift carry the
 * affordance. */
.stl-tests-row__mark {
  position: relative;
  display: flex; align-items: center;
  /* In the Tests panel context the wordmark is compact — icon at
   * 36px with the acronym inline next to it. */
  --stl-mark-icon-size: 36px;
  color: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 1);
}
.stl-tests-row__mark .stl-test-mark {
  flex-direction: row;
  align-items: center;
  gap: 12px;
}
.stl-tests-row__mark .stl-test-mark__name {
  font-size: 1.25rem;
  color: var(--stl-text);
}
.stl-tests-row__meta { position: relative; display: grid; gap: 2px; min-width: 0; }
.stl-tests-row__name { font-size: 1rem; font-weight: 600; color: var(--stl-text); }
.stl-tests-row__tagline { font-size: 0.85rem; color: var(--stl-text-muted); }
.stl-tests-row__counts {
  position: relative;
  display: flex; gap: var(--s-3);
}
.stl-tests-row__counts > div {
  display: grid; gap: 2px; text-align: right;
  min-width: 60px;
}
.stl-tests-row__counts dt {
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--stl-text-subtle); font-family: var(--stl-font-mono);
}
.stl-tests-row__counts dd {
  font-size: 1.1rem; font-weight: 600; color: var(--stl-text);
  font-variant-numeric: tabular-nums; margin: 0;
}

/* Switch component — used by the Tests panel Active/Hidden toggle.
 * Pure CSS, no extra deps. ARIA: button[role=switch] with
 * aria-checked driving the visual state. */
.stl-switch {
  appearance: none; -webkit-appearance: none;
  background: transparent; border: 0; padding: 4px 10px 4px 4px;
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 10px;
  font: inherit; color: var(--stl-text);
  border-radius: var(--r-pill);
  position: relative;
}
.stl-switch:focus-visible { outline: 2px solid var(--stl-accent); outline-offset: 2px; }
.stl-switch__track {
  position: relative;
  width: 38px; height: 22px;
  border-radius: var(--r-pill);
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
  transition: background var(--dur-base) ease, border-color var(--dur-base) ease;
  flex-shrink: 0;
}
.stl-switch__thumb {
  position: absolute;
  top: 2px; left: 2px;
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--stl-text-muted);
  transition: transform var(--dur-base) cubic-bezier(0.34, 1.56, 0.64, 1),
              background var(--dur-base) ease;
}
.stl-switch__lbl {
  font-size: 12px;
  font-family: var(--stl-font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  font-weight: 500;
  min-width: 50px;
}
/* Tracks the global per-test accent (--stl-tint) by default so the
 * score-screen "Include lower difficulty questions" switch picks up
 * the test's color. Inside the admin Tests panel, each row sets its
 * own --tile-tint inline, which we honor via the CSS-variable cascade
 * (the inline tile-tint becomes the value of --stl-tint inside that
 * row scope). */
.stl-switch[aria-checked="true"] .stl-switch__track {
  background: rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.25);
  border-color: rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.6);
}
.stl-switch[aria-checked="true"] .stl-switch__thumb {
  background: rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 1);
  transform: translateX(16px);
}
/* (Removed: .stl-tests-row { --stl-tint: var(--tile-tint) } —
 * that rule made every toggle inside an admin test row paint in
 * the test's color, breaking the "admin is monotone lavender"
 * design rule. Per-test identity now lives on the wordmark icon
 * and the row's backdrop ::before gradient (both pull --tile-tint
 * directly), not on the toggle chrome.) */
.stl-switch[aria-checked="true"] .stl-switch__lbl {
  color: var(--stl-text);
}

@media (max-width: 640px) {
  .stl-tests-row {
    grid-template-columns: 1fr;
    text-align: left;
    gap: var(--s-2);
  }
  .stl-tests-row__counts { justify-self: start; }
  .stl-tests-row__counts > div { text-align: left; }
}

/* Math markup produced by renderMath in app.js. Two classes:
 *   .stl-math-fracsup — a stacked-fraction superscript for `^(p/q)`
 *     (e.g. a^(1/2) shows ½ as a proper fraction in superscript size)
 *   .stl-math-sup — a parenthesized non-fraction exponent like ^(4+1)
 * The fraction stacks numerator over denominator with a thin rule
 * between, then sits at superscript baseline next to the base term. */
.stl-math-fracsup {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  vertical-align: 0.55em;       /* superscript-ish baseline */
  font-size: 0.65em;
  line-height: 1;
  margin: 0 1px 0 1px;
  font-style: italic;            /* match math typography */
}
.stl-math-num,
.stl-math-den {
  text-align: center;
  padding: 0 2px;
}
.stl-math-num {
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
}
.stl-math-den {
  padding-top: 1px;
}
.stl-math-sup {
  font-style: italic;
}

/* Display-style stacked fraction — produced by renderMath when a bank
 * entry uses `\frac{a}{b}` syntax. Renders the numerator above a
 * horizontal rule above the denominator, sized at the surrounding text
 * height so equations like (2x − 5)/3 read as proper textbook fractions
 * rather than slash notation.
 *
 * Bank authors opt in per question: existing inline `2/3` text in any
 * of the 2,825 banked questions remains unchanged. */
.stl-math-displayfrac {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  /* vertical-align: middle anchors the box's vertical CENTER to the
   * baseline + half-x-height of the parent — which is exactly where
   * the fraction bar sits inside this stacked layout. The previous
   * -0.45em pin offset the BOTTOM of the box and ended up parking the
   * bar above the text's x-height midline; once the surrounding
   * line-height stretched to accommodate the fraction, the text and
   * math read as drifting apart. `middle` keeps them locked together
   * regardless of how tall the fraction grows.
   *
   * The tiny -0.08em nudge corrects for a thin asymmetry in the box's
   * internal vertical centre — the 1px border on the numerator's
   * bottom edge + the 2px top/bottom paddings don't sum to zero, so
   * without the nudge the bar floats ~1px above x-height-middle. */
  vertical-align: middle;
  margin-top: -0.08em;
  font-size: 1em;
  line-height: 1.05;
  margin-right: 2px;
  margin-left: 2px;
  /* Slight horizontal padding so the rule extends past the digits. */
  padding: 0 1px;
}
.stl-math-displayfrac__num,
.stl-math-displayfrac__den {
  text-align: center;
  padding: 0 4px;
  /* Italic isn't forced here — variables inside already wear <em>;
   * digits and operators stay upright (matches MathJax/textbook style). */
}
.stl-math-displayfrac__num {
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
}
.stl-math-displayfrac__den {
  padding-top: 2px;
}

/* Square root — radical sign + radicand with a vinculum (top bar)
 * connecting the two. The √ sits at the surrounding baseline; the
 * radicand wears a top border that visually extends the radical's
 * horizontal stroke. Authored as `\sqrt{x+1}` in the bank. */
.stl-math-sqrt {
  display: inline-flex;
  align-items: stretch;
  vertical-align: baseline;
  margin: 0 1px;
}
.stl-math-sqrt__radical {
  font-style: normal;
  /* Slight upscale so the √ glyph reads at the same optical weight as
   * the radicand. */
  font-size: 1.1em;
  line-height: 1;
  padding-right: 1px;
}
.stl-math-sqrt__body {
  border-top: 1px solid currentColor;
  padding: 0 3px;
  /* Push the body slightly down so the top border aligns with the top
   * of the radical glyph. */
  margin-top: 0.15em;
}

.stl-quiz__choices {
  display: grid;
  gap: var(--s-2);
  border: 0;
  padding: 0;
  margin: 0;
}
.stl-choice {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 11px var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  cursor: pointer;
  /* Hidden lavender accent bar lives on the left edge — grows in on
   * hover, signals "this is your pick if you click" without
   * committing to a selection until you do. */
  overflow: hidden;
  transition: border-color var(--dur-base) var(--ease-spring),
              background   var(--dur-base) var(--ease-spring),
              transform    var(--dur-fast) var(--ease-spring);
}
.stl-choice::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--stl-accent);
  transform: scaleY(0);
  transform-origin: center;
  transition: transform var(--dur-base) var(--ease-spring);
}
.stl-choice:hover {
  border-color: var(--stl-accent-line);
  background: var(--stl-surface-2);
}
.stl-choice:hover::before { transform: scaleY(0.6); }
.stl-choice.is-selected::before { transform: scaleY(1); }
@media (hover: none) {
  .stl-choice:hover { border-color: var(--stl-hairline); background: var(--stl-surface); }
  .stl-choice:hover::before { transform: scaleY(0); }
}
.stl-choice input { position: absolute; opacity: 0; pointer-events: none; }
.stl-choice__letter {
  flex: 0 0 auto;
  width: 22px; height: 22px;
  border-radius: 6px;
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline-hi);
  font-family: var(--stl-font-mono);
  font-weight: 500;
  font-size: 11px;
  display: grid;
  place-items: center;
  color: var(--stl-text-muted);
}
.stl-choice__text { flex: 1; font-size: 0.97rem; line-height: 1.45; }
/* In the choice-row context, the A/B/C/D chip is flex-centered (its
 * geometric center sits on the line box's geometric center). A
 * stacked-fraction inside .stl-choice__text uses vertical-align:
 * middle which parks the bar at the math axis.
 *
 * Measured in Chrome on the deployed page (with the global
 * margin-top: -0.08em already in effect): the bar sits 0.74px ABOVE
 * chip center. Any additional negative translate moves it further
 * above. Smallest correction: don't add any choice-context lift at
 * all. The 0.74px residual is sub-pixel-rounding territory and
 * reads as aligned.
 *
 * (Rule kept as an explicit no-op so the choice context is documented
 * — anyone tempted to add a translate here can read the tuning log.) */
.stl-choice .stl-math-displayfrac {
  /* no transform — measured baseline is already within 1px of chip
   * center. See tuning notes above. */
}
.stl-choice.is-selected {
  border-color: var(--stl-accent-line);
  background: var(--stl-accent-soft);
}
.stl-choice.is-selected .stl-choice__letter {
  background: var(--stl-accent);
  color: var(--stl-bg-deep);
  border-color: var(--stl-accent);
}
/* FIXLIST P0 (2026-08-18): was var(--stl-accent-soft) — a 12%-alpha ring,
 * effectively invisible. The answer control on the core screen needs a real
 * focus indicator. :focus-within because the real focus target is the
 * visually-hidden radio inside the label. */
.stl-choice:focus-within {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}

/* Admin review reuse — an opened question renders with the participant
 * .stl-choice styling so it mirrors the test view. Read-only (not
 * clickable), and the correct answer is flagged with the green signal
 * rather than the lavender "selected" state. Hover animation is muted
 * since these rows aren't interactive. */
.stl-review-card__quiz .stl-choice { cursor: default; }
.stl-review-card__quiz .stl-choice:hover {
  border-color: var(--stl-hairline);
  background: var(--stl-surface);
}
.stl-review-card__quiz .stl-choice:hover::before { transform: scaleY(0); }
.stl-choice.is-correct {
  border-color: rgba(var(--stl-green-rgb), 0.55);
  background: rgba(var(--stl-green-rgb), 0.10);
}
.stl-review-card__quiz .stl-choice.is-correct:hover {
  border-color: rgba(var(--stl-green-rgb), 0.55);
  background: rgba(var(--stl-green-rgb), 0.10);
}
.stl-choice.is-correct .stl-choice__letter {
  background: var(--stl-green);
  border-color: var(--stl-green);
  color: var(--stl-bg-deep);
}
.stl-choice__mark {
  margin-left: auto;
  font-weight: 700;
  color: #4ade80;
}

/* --- figure (question images and tables) ----------------------------
 * Figures come in two shapes:
 *   data-kind="image" — screenshot; needs the canonical CSS dark-mode-
 *     screenshot trick (invert + hue-rotate) so the white background
 *     becomes a comfortable off-black instead of a blowout against the
 *     canvas. A subtle brightness adjust matches the surface tone.
 *   data-kind="table" — native HTML table; styled with the same hairline
 *     borders, mono labels, and tabular numerals as the rest of the
 *     dark theme. Doesn't need filter hackery, so the surface tile here
 *     just frames the table without padding being load-bearing.
 */
.stl-quiz__figure {
  display: block;
  margin: 0 0 var(--s-4);
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  padding: var(--s-3);
  text-align: center;
  overflow: hidden;
}
.stl-quiz__figure[hidden] { display: none; }
.stl-quiz__figure[data-kind="image"] img {
  display: inline-block;
  max-width: 100%;
  height: auto;
  border-radius: var(--r-sm);
  filter: invert(0.92) hue-rotate(180deg) brightness(0.95);
  /* a tiny 0.05 short of full inversion keeps an off-black background
   * after invert, which sits more comfortably against the card surface
   * than absolute black would. */
}
.stl-quiz__figure[data-kind="table"] {
  /* Tables already have their own structure — drop the outer surface
   * tile so the table reads as part of the question card, not a panel
   * inside it. */
  background: transparent;
  border: 0;
  padding: 0;
  text-align: left;
}
.stl-quiz__figure[data-kind="svg"] {
  /* Inline SVG figures (geometric diagrams). Same surface tile as the
   * default figure container, but the inner SVG uses currentColor so
   * lines / text inherit the canvas text color. Variable labels inside
   * the SVG (line names, function names) get the same italic-lavender
   * treatment as stem variables. */
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  padding: var(--s-4);
}
.stl-quiz__figure[data-kind="svg"] svg {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  height: auto;
  color: var(--stl-text);
  font-family: "Geist", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  font-size: 15px;
}
.stl-quiz__figure[data-kind="svg"] .stl-svg-var {
  font-style: italic;
  fill: var(--stl-accent);
  font-weight: 500;
}

.stl-quiz__figure[data-kind="chart"] {
  /* Chart.js needs an explicitly-sized parent for responsive sizing.
   * The aspect ratio here matches the rough shape of the SAT graph
   * boxes so converted charts feel proportional to the original.
   * Height accounts for the bumped tick / axis-title / chart-title
   * font sizes (14 / 13 / 15 px) so the plot area itself stays
   * readable. */
  position: relative;
  height: 380px;
  padding: var(--s-3) var(--s-3) var(--s-2);
}
.stl-quiz__figure[data-kind="chart"] canvas {
  display: block;
  max-width: 100%;
  height: 100% !important;
  width: 100% !important;
}
@media (max-width: 560px) {
  .stl-quiz__figure[data-kind="chart"] { height: 320px; }
}

/* --- HTML table inside a figure ------------------------------------- */
.stl-quiz__table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  overflow: hidden;
}
.stl-quiz__table caption {
  caption-side: top;
  text-align: left;
  padding: 0 0 var(--s-2);
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-quiz__table th,
.stl-quiz__table td {
  padding: 10px var(--s-3);
  border-bottom: 1px solid var(--stl-hairline);
  text-align: right;          /* numeric columns want right-align */
  color: var(--stl-text);
  vertical-align: middle;
}
.stl-quiz__table th {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  font-weight: 500;
  background: var(--stl-surface-2);
  border-bottom-color: var(--stl-hairline-hi);
}
/* First column tends to be a label (e.g., "Lion", "Washington"); left-align
 * to match reading direction. */
.stl-quiz__table th:first-child,
.stl-quiz__table td:first-child {
  text-align: left;
  color: var(--stl-text);
}
/* Row-scope <th> (when firstColIsHeader is set on the question) gets
 * lighter weight to match data cells but stay scannable. */
.stl-quiz__table tbody th {
  background: transparent;
  font-family: inherit;
  font-size: 0.95rem;
  text-transform: none;
  letter-spacing: 0;
  color: var(--stl-text);
  font-weight: 500;
}
.stl-quiz__table tbody tr:last-child th,
.stl-quiz__table tbody tr:last-child td {
  border-bottom: 0;
}
/* Optional totals row: top-border emphasizes it as a sum/summary line */
.stl-quiz__table tbody tr.stl-quiz__table-totals th,
.stl-quiz__table tbody tr.stl-quiz__table-totals td {
  border-top: 1px solid var(--stl-hairline-hi);
  font-weight: 600;
  background: var(--stl-surface);
}
@media (max-width: 560px) {
  .stl-quiz__table { font-size: 0.9rem; }
  .stl-quiz__table th,
  .stl-quiz__table td { padding: 8px 10px; }
}

/* ====================================================================
 * CLASSIC view — SAT-style ruled grid (the DEFAULT table look).
 * A compact table sized to its content and centered, with full
 * horizontal AND vertical grid lines on every cell and a plain
 * (non-mono, non-uppercase) header — the look of a printed SAT table.
 * Toggling to Modern (data-view="modern") falls back to the rounded
 * row-separator styling above. Gated on :root[data-view="classic"],
 * which the boot script stamps by default.
 * ================================================================== */
:root            { --stl-classic-line: rgba(255, 255, 255, 0.9); }
:root[data-theme="light"] { --stl-classic-line: rgba(15, 23, 42, 0.9); }

:root[data-view="classic"] .stl-quiz__figure[data-kind="table"] { text-align: center; }
:root[data-view="classic"] .stl-quiz__table {
  display: inline-table;
  width: auto;                       /* shrink to content, not full width */
  /* `separate` (not collapse) so the outer border-radius actually rounds
   * the table's corners — collapsed borders ignore border-radius. The
   * inner grid lines are drawn as right/bottom borders on each cell,
   * with the last column/row stripped to avoid doubling the outer
   * border. overflow:hidden clips any sub-pixel overshoot at the
   * rounded corners. */
  border-collapse: separate;
  border-spacing: 0;
  border: 2px solid var(--stl-classic-line);
  border-radius: 10px;
  overflow: hidden;
  background: transparent;
  margin: 4px auto;
  text-align: center;
}
:root[data-view="classic"] .stl-quiz__table caption {
  font-family: inherit;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.85rem;
  color: var(--stl-text-subtle);
  text-align: center;
  padding-bottom: var(--s-2);
}
:root[data-view="classic"] .stl-quiz__table th,
:root[data-view="classic"] .stl-quiz__table td {
  /* Inner grid lines as right + bottom on each cell (2px to match the
   * outer border thickness). The last column/row strip these below so
   * the inside lines don't double the outer table border. */
  border: 0;
  border-right: 2px solid var(--stl-classic-line);
  border-bottom: 2px solid var(--stl-classic-line);
  padding: 8px 18px;
  text-align: center;
  color: var(--stl-text);
  background: transparent;
}
:root[data-view="classic"] .stl-quiz__table th:last-child,
:root[data-view="classic"] .stl-quiz__table td:last-child { border-right: 0; }
:root[data-view="classic"] .stl-quiz__table th {
  font-family: inherit;              /* plain header, not mono uppercase */
  font-size: 0.95rem;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
  color: var(--stl-text);
  background: transparent;
}
:root[data-view="classic"] .stl-quiz__table th:first-child,
:root[data-view="classic"] .stl-quiz__table td:first-child { text-align: left; }
:root[data-view="classic"] .stl-quiz__table tbody th { font-weight: 600; }
/* Strip the bottom border on the last row so it doesn't double the
 * outer table border at the rounded bottom corners. */
:root[data-view="classic"] .stl-quiz__table tbody tr:last-child th,
:root[data-view="classic"] .stl-quiz__table tbody tr:last-child td {
  border-bottom: 0;
}
:root[data-view="classic"] .stl-quiz__table tbody tr.stl-quiz__table-totals th,
:root[data-view="classic"] .stl-quiz__table tbody tr.stl-quiz__table-totals td {
  border-top: 2px solid var(--stl-classic-line);
  font-weight: 700;
  background: transparent;
}

/* Equal-width columns for uniform data tables (variable-shape: x|y,
 * t|f(t), etc.). The JS heuristic in renderFigure sets the class and
 * a `--stl-table-col-min` ch-based length derived from the widest
 * cell. We apply that as min-width to every cell; since cells fit
 * within it, every column collapses to the same width. Multi-word
 * tables (two-way frequency, etc.) don't get the class and keep
 * their natural auto widths. Applies in both Classic and Modern. */
.stl-quiz__table--equal-cols th,
.stl-quiz__table--equal-cols td {
  min-width: var(--stl-table-col-min, 4ch);
  text-align: center;
}
/* Beat the classic-view :first-child left-align rule (uniform data
 * tables center every column, including the first — for x|y / t|f(t)
 * shapes a left-aligned first column would look off). */
:root .stl-quiz__table--equal-cols th:first-child,
:root .stl-quiz__table--equal-cols td:first-child {
  text-align: center;
}

/* ====================================================================
 * MODERN view — colorful brand-styled SVG figures.
 *
 * The imported SVGs draw geometry with hardcoded dark strokes (great
 * for the Classic, SAT-style look). In Modern view we want our brand
 * identity instead: the figure tile picks up a soft accent gradient,
 * lines/curves use the accent color at 2px, dots/markers fill with the
 * accent, and any hardcoded white background rect gets cleared so the
 * tile's gradient shows through. CSS overrides the SVG presentation
 * attributes (`stroke="#222"`, `fill="#ffffff"`, etc.) — those are the
 * lowest-specificity styles, so any matching rule wins.
 * Classic view leaves all of this alone and the original dark strokes
 * keep the SAT look. ================================================ */
:root[data-view="modern"] .stl-quiz__figure[data-kind="svg"] {
  background: linear-gradient(135deg, var(--stl-accent-soft), var(--stl-surface));
  border-color: var(--stl-accent-line);
}
:root[data-view="modern"] .stl-quiz__figure[data-kind="svg"] svg line,
:root[data-view="modern"] .stl-quiz__figure[data-kind="svg"] svg polygon,
:root[data-view="modern"] .stl-quiz__figure[data-kind="svg"] svg path,
:root[data-view="modern"] .stl-quiz__figure[data-kind="svg"] svg polyline {
  stroke: var(--stl-accent);
  stroke-width: 2;
}
:root[data-view="modern"] .stl-quiz__figure[data-kind="svg"] svg circle {
  fill: var(--stl-accent);
  stroke: none;
}
:root[data-view="modern"] .stl-quiz__figure[data-kind="svg"] svg text {
  fill: var(--stl-text);
}
/* Clear the hardcoded background rect (`<rect fill="#ffffff">` or
 * `fill="transparent"` sitting at the top of nearly every imported SVG)
 * so the figure tile's accent gradient shows through. The bg rect is
 * always the first direct child. */
:root[data-view="modern"] .stl-quiz__figure[data-kind="svg"] svg > rect:first-child {
  fill: transparent;
}

/* View-toggle icon swap: grid glyph in Classic, rows glyph in Modern. */
.stl-view-toggle__icon--rows { display: none; }
.stl-view-toggle__icon--grid { display: inline-flex; }
:root[data-view="modern"] .stl-view-toggle__icon--grid { display: none; }
:root[data-view="modern"] .stl-view-toggle__icon--rows { display: inline-flex; }

/* --- grid-in (free-response / student-produced answer) -------------- */
/* --- chart-grid choices (multiple-choice with graph options) ---------
 * Used when a question has 4 candidate graphs as options. Each option
 * is a small Chart.js canvas in a card with the letter (A/B/C/D)
 * pinned at top-left. 2-column grid on desktop, 1-column on phones
 * (still readable; charts get a touch shorter). Selection / hover
 * states mirror the regular .stl-choice text choices. */
.stl-choice-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-2);
  margin-top: var(--s-3);
}
.stl-choice-grid[hidden] { display: none; }

.stl-choice--chart {
  position: relative;
  display: block;
  padding: var(--s-3);
  padding-top: 36px;            /* room for letter chip */
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-out),
              background   var(--dur-base) var(--ease-out);
  height: 200px;
}
.stl-choice--chart:hover {
  border-color: var(--stl-hairline-hi);
  background: var(--stl-surface-2);
}
.stl-choice--chart.is-selected {
  border-color: var(--stl-accent-line);
  background: var(--stl-accent-soft);
}
.stl-choice--chart .stl-choice__letter {
  position: absolute;
  top: var(--s-3);
  left: var(--s-3);
  z-index: 1;
}
.stl-choice--chart.is-selected .stl-choice__letter {
  background: var(--stl-accent);
  color: var(--stl-bg-deep);
  border-color: var(--stl-accent);
}
.stl-choice--chart canvas {
  display: block;
  width: 100% !important;
  height: 100% !important;
  max-height: calc(100% - 0px);
}

/* Review-mode marks on chart choices: green outline for correct, red
 * for picked-wrong. Mirrors the .stl-review__choices li[data-state] CSS. */
.stl-choice--chart[data-state="correct"] {
  border-color: rgba(var(--stl-green-rgb), 0.45);
  background: rgba(var(--stl-green-rgb), 0.06);
}
.stl-choice--chart[data-state="picked-wrong"] {
  border-color: rgba(var(--stl-red-rgb), 0.45);
  background: rgba(var(--stl-red-rgb), 0.06);
}

@media (max-width: 560px) {
  .stl-choice-grid { grid-template-columns: 1fr; }
  .stl-choice--chart { height: 180px; }
}

.stl-quiz__gridin { display: grid; gap: var(--s-2); margin-top: var(--s-2); }
.stl-quiz__gridin[hidden] { display: none; }
.stl-quiz__gridin-label {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-quiz__gridin-input {
  width: 100%;
  padding: 14px 16px;
  font-size: 1.15rem;
  font-weight: 500;
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  color: var(--stl-text);
  background: var(--stl-bg);
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-md);
  outline: none;
  transition: border-color var(--dur-base) var(--ease-out),
              background  var(--dur-base) var(--ease-out),
              box-shadow  var(--dur-base) var(--ease-out);
}
.stl-quiz__gridin-input:hover { border-color: var(--stl-hairline-hover); }
.stl-quiz__gridin-input:focus,
.stl-quiz__gridin-input:focus-visible {
  border-color: var(--stl-accent);
  background: var(--stl-bg-2);
  box-shadow: 0 0 0 4px var(--stl-accent-soft);
}
.stl-quiz__gridin-input.is-locked {
  background: var(--stl-accent-soft);
  border-color: var(--stl-accent);
  color: var(--stl-text);
}
/* Inline error state — fired when the user hits Green/Yellow without
 * typing. Red border + soft red glow + a brief shake to draw the eye. */
.stl-quiz__gridin-input.is-error,
.stl-quiz__gridin-input.is-error:hover,
.stl-quiz__gridin-input.is-error:focus,
.stl-quiz__gridin-input.is-error:focus-visible {
  border-color: var(--stl-red);
  background: rgba(var(--stl-red-rgb), 0.05);
  box-shadow: 0 0 0 4px rgba(var(--stl-red-rgb), 0.18);
  animation: stl-error-shake 360ms cubic-bezier(0.36, 0.07, 0.19, 0.97);
}
.stl-quiz__gridin-hint {
  font-size: 13px;
  color: var(--stl-text-subtle);
  margin: 0;
}

/* --- signal (the only place green/yellow/red appear in the UI) -------- */
.stl-quiz__signal { display: grid; gap: var(--s-3); }
.stl-quiz__signal-help {
  margin: 0;
  text-align: center;
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  /* Reserve room for the slightly longer error message so the layout
   * doesn't reflow when the helper swaps to error text. */
  min-height: 1.4em;
  transition: color var(--dur-base) var(--ease-out);
}
/* Error voice for the signal help line. Sits in the same slot as the
 * default help text but reads as an inline error rather than a modal. */
.stl-quiz__signal-help.is-error {
  color: var(--stl-red);
  letter-spacing: 0.06em;
  text-transform: none;
  font-size: 12px;
  font-weight: 500;
  animation: stl-error-fadein 200ms var(--ease-out);
}

/* MC choices error halo — applies to both the radio fieldset and the
 * choiceCharts grid (q-7029, q-7014). Soft red drop-shadow rather than
 * a hard border so the individual choice rows still read as their own
 * affordances; the halo just frames "you need to pick something." */
.stl-quiz__choices.is-error,
.stl-choice-grid.is-error {
  border-radius: var(--r-md);
  box-shadow: 0 0 0 2px rgba(var(--stl-red-rgb), 0.45),
              0 12px 30px -12px rgba(var(--stl-red-rgb), 0.35);
  animation: stl-error-shake 360ms cubic-bezier(0.36, 0.07, 0.19, 0.97);
}

@keyframes stl-error-shake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX( 2px); }
  30%, 50%, 70% { transform: translateX(-3px); }
  40%, 60% { transform: translateX( 3px); }
}
@keyframes stl-error-fadein {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .stl-quiz__gridin-input.is-error,
  .stl-quiz__choices.is-error,
  .stl-choice-grid.is-error,
  .stl-quiz__signal-help.is-error {
    animation: none;
  }
}
.stl-signal {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s-2);
}
.stl-signal__btn {
  position: relative;
  display: grid;
  justify-items: start;
  align-content: space-between;
  gap: var(--s-3);
  padding: var(--s-4);
  min-height: 92px;
  border-radius: var(--r-lg);
  border: 1px solid var(--stl-hairline-hi);
  background: var(--stl-surface);
  color: var(--stl-text);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  transition: transform var(--dur-fast) var(--ease-out),
              border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}
.stl-signal__btn {
  /* Re-state the transition list to include box-shadow so the hover
   * halo can fade in cleanly. */
  transition: transform    var(--dur-fast) var(--ease-spring),
              border-color var(--dur-base) var(--ease-spring),
              background   var(--dur-base) var(--ease-spring),
              box-shadow   var(--dur-base) var(--ease-spring);
}
.stl-signal__btn:hover {
  transform: translateY(-2px);
  background: var(--stl-surface-2);
}
.stl-signal__btn:active { transform: translateY(0); transition-duration: var(--dur-fast); }
.stl-signal__btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
.stl-signal__btn:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
/* Chip ramps brighter on hover — `currentColor` already drives this,
 * we just punch up the box-shadow for a stronger glow. */
.stl-signal__chip {
  width: 12px; height: 12px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 12px currentColor;
  transition: box-shadow var(--dur-base) var(--ease-spring);
}
.stl-signal__btn:hover .stl-signal__chip { box-shadow: 0 0 18px currentColor; }
.stl-signal__lbl {
  font-weight: 600;
  font-size: 0.95rem;
  letter-spacing: -0.005em;
  color: var(--stl-text);
}
.stl-signal__sub {
  font-size: 0.8rem;
  color: var(--stl-text-muted);
  line-height: 1.35;
}
.stl-signal__btn--green  { color: var(--stl-green); }
.stl-signal__btn--yellow { color: var(--stl-yellow); }
.stl-signal__btn--red    { color: var(--stl-red); }

/* Per-color hover halo — colored drop shadow + matching border. The
 * halo is what makes hover feel "alive" — it tells you which signal
 * you're about to fire before you commit. */
.stl-signal__btn--green:hover {
  border-color: rgba(var(--stl-green-rgb), 0.7);
  box-shadow: 0 14px 36px -10px rgba(var(--stl-green-rgb), 0.45);
}
.stl-signal__btn--yellow:hover {
  border-color: rgba(var(--stl-yellow-rgb), 0.7);
  box-shadow: 0 14px 36px -10px rgba(var(--stl-yellow-rgb), 0.45);
}
.stl-signal__btn--red:hover {
  border-color: rgba(var(--stl-red-rgb), 0.7);
  box-shadow: 0 14px 36px -10px rgba(var(--stl-red-rgb), 0.45);
}
@media (hover: none) {
  .stl-signal__btn:hover { transform: none; box-shadow: none; background: var(--stl-surface); }
  .stl-signal__btn:hover .stl-signal__chip { box-shadow: 0 0 12px currentColor; }
  .stl-signal__btn--green:hover,
  .stl-signal__btn--yellow:hover,
  .stl-signal__btn--red:hover { border-color: var(--stl-hairline-hi); }
}

/* .stl-skip was the lone underlined "Skip this question" link. Skip is now
 * the forward half of .stl-stepnav (see above) and inherits its styling,
 * so the standalone rules are gone. */

/* --- results bento --------------------------------------------------- */
.stl-results { display: grid; gap: var(--gap-section); }
/* Header row pairs the eyebrow ("Results") with a color-coded test
 * badge on the right so the user knows which test they're reviewing
 * without having to drop their eyes to find the testType anywhere
 * else. The eyebrow keeps its dot+text structure; the badge floats
 * to the trailing edge. On narrow widths it wraps below the eyebrow. */
.stl-results__head-row {
  display: flex;
  align-items: center;
  /* Single-row pill cluster (badge + pacing + time-ago) now lives
   * in one flex group inside .stl-results__pacing — head-row just
   * holds it. Wrap so a narrow viewport stacks pills onto a 2nd line
   * cleanly instead of horizontally scrolling. */
  gap: var(--s-3);
  flex-wrap: wrap;
}

/* ---- shared color-coded test-type badge ---------------------------
 * Compact pill that surfaces "this thing belongs to ACT/SAT/etc."
 * Used on the Results header, the Review meta-row, and history rows.
 * Caller sets --badge-tint inline (an "r, g, b" triplet from the
 * test's tint) so each instance can render in its test's color. */
.stl-test-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 6px;
  border-radius: var(--r-pill);
  border: 1px solid rgba(var(--badge-tint, var(--stl-rainbow-mid-rgb)), 0.45);
  background: rgba(var(--badge-tint, var(--stl-rainbow-mid-rgb)), 0.08);
  color: rgba(var(--badge-tint, var(--stl-rainbow-mid-rgb)), 1);
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  flex-shrink: 0;
  transition: background var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
.stl-test-badge__icon {
  display: inline-flex;
  width: 14px;
  height: 14px;
  color: inherit;
  flex-shrink: 0;
}
.stl-test-badge__icon svg {
  width: 100%;
  height: 100%;
  display: block;
  fill: currentColor;
}
.stl-test-badge__name {
  display: inline-block;
  line-height: 1;
}
.stl-results__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s-3);
}

/* Test pacing meta-line — sits between the results sub-paragraph and
 * the bento grid. Gives time context (total + per-question avg) as
 * metadata about *the attempt*, separate from the categorical tiles
 * below which describe correctness outcomes. Mono numerals so the
 * digits feel like a stopwatch reading; muted labels so it doesn't
 * shout against the H1. */
.stl-results__meta {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 8px;
  margin: 0 0 var(--s-2);
  padding: 0;
  font-family: var(--stl-font-mono);
  font-size: 13px;
  color: var(--stl-text-subtle);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
.stl-results__meta-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--stl-text-faint);
  display: inline-block;
  align-self: center;
  margin-right: 2px;
}
.stl-results__meta-num {
  color: var(--stl-text);
  font-weight: 500;
  font-size: 14.5px;       /* the numeric values get slight emphasis */
}
.stl-results__meta-lbl {
  color: var(--stl-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 11px;
}
.stl-results__meta-sep {
  color: var(--stl-text-faint);
  margin: 0 2px;
}
/* =========================================================
 * RESULTS OVERVIEW — single hairline-bordered card containing
 * the hero (score + proportional meter) and the 3-row ledger,
 * separated by an internal hairline divider. The two halves
 * read as one continuous surface ("one card, two zones") so
 * the score and the per-signal breakdown feel like a single
 * unit instead of two stacked tiles. Score folds the "Correct"
 * count inline ("N/T correct"); the ledger shows mastery /
 * lucky / review with count + percentage.
 * ========================================================= */
.stl-results__overview[data-merged="true"] {
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  overflow: hidden;          /* corner-clip the inner zones */
  display: grid;
  /* Hero zone fills leftover vertical room (1fr), ledger stays at
   * its natural compact height (auto). When the chart tile stretches
   * this card, the score + meter get extra breathing room rather
   * than the ledger rows growing tall. */
  grid-template-rows: 1fr auto auto;
  gap: 0;
}
.stl-results__overview-divider {
  height: 1px;
  background: var(--stl-hairline);
}

/* ---- Hero zone (top of merged card) -----------------------------
 * No own border / background — the merged container owns the
 * surface. Internal padding only. Tightened from 22/16 to
 * 20/14 so the score + meter feel more anchored to the ledger
 * below instead of floating in whitespace.
 */
.stl-results__hero {
  /* Inside the merged container — surface is provided by the parent. */
  padding: 20px 22px;
  display: grid;
  gap: 14px;
}
/* Legacy non-merged hero (kept for any other consumer) keeps its own
 * surface so a standalone .stl-results__hero still renders. */
.stl-results__overview:not([data-merged="true"]) .stl-results__hero {
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
}
.stl-results__hero-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
}
.stl-results__score {
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
  line-height: 1;
}
.stl-results__score-num {
  font-size: clamp(2.4rem, 6vw, 3rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  color: var(--stl-text);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.stl-results__score-word {
  font-size: clamp(0.95rem, 2.2vw, 1.1rem);
  font-weight: 500;
  color: var(--stl-text-muted);
  letter-spacing: 0.01em;
}
.stl-results__hero-meta {
  font-size: 13px;
  color: var(--stl-text-muted);
  font-variant-numeric: tabular-nums;
}

/* ---- Proportional meter bar -------------------------------------
 * 10px tall, fully-rounded ends, 2px gap between segments. Each
 * segment uses inline `flex: <count>` so its width matches its
 * share of total questions. A segment with flex:0 collapses to
 * nothing (no slim sliver, no empty placeholder). */
.stl-results__meter {
  height: 10px;
  border-radius: 5px;
  overflow: hidden;
  display: flex;
  gap: 2px;
  background: rgba(255, 255, 255, 0.04);
}
.stl-results__meter-seg {
  display: block;
  height: 100%;
  transition: flex var(--dur-base) var(--ease-out);
}
.stl-results__meter-seg--mastery       { background: var(--stl-green); }
.stl-results__meter-seg--misconception { background: var(--stl-red); }
.stl-results__meter-seg--lucky         { background: var(--stl-yellow); }
.stl-results__meter-seg--gap           { background: var(--stl-amber); }
.stl-results__meter-seg--fluke         { background: rgba(var(--stl-red-rgb), 0.55); }
.stl-results__meter-seg--cold          { background: var(--stl-text-faint); }

/* ---- Ledger rows ------------------------------------------------
 * Single hairline-bordered card with 3 rows separated by 1px
 * rules. Each row is a button (jumps to that bucket in review)
 * with a colored dot, label + parenthetical hint, count, and
 * percentage on the right. */
/* Ledger zone — inside the merged overview the parent owns the
 * surface and border, so the ledger drops its own. Standalone
 * usage (non-merged) keeps its bordered card look. */
.stl-results__ledger {
  overflow: hidden;
  /* Rows stay at their natural compact height — the hero zone
   * absorbs any extra vertical space the chart tile adds. */
  display: grid;
}
.stl-results__overview:not([data-merged="true"]) .stl-results__ledger {
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
}
.stl-results__row {
  /* Reset button defaults, layer back interactive surface. */
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  color: var(--stl-text);
  text-align: left;
  width: 100%;
  background: transparent;
  border: 0;
  border-top: 1px solid var(--stl-hairline);
  padding: 10px 18px;
  /* 2-col grid now (label | stats). The num + pct pill sit together
   * inside .stl-results__row-stats so they read as one unit instead
   * of being thrown to opposite edges of a 3-col grid. */
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 12px;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}
.stl-results__row:first-child { border-top: 0; }
.stl-results__row:hover { background: rgba(255, 255, 255, 0.03); }
.stl-results__row:focus-visible {
  outline: 2px solid var(--row-color, var(--stl-accent));
  outline-offset: -2px;
}
.stl-results__row:disabled,
.stl-results__row[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.4;
}
.stl-results__row:disabled:hover,
.stl-results__row[aria-disabled="true"]:hover { background: transparent; }

/* Label is a 2-column grid: colored dot (col 1, spans both rows
 * for vertical centering) + a text stack of title (row 1) and hint
 * (row 2). Stacking the text keeps the cell legible when the
 * overview column is narrow (chart is dominant in the 2-col layout). */
.stl-results__row-label {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  column-gap: 10px;
  align-items: center;
  min-width: 0;
}
.stl-results__row-label::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--row-color, var(--stl-text-faint));
  grid-column: 1;
  grid-row: 1 / span 2;
  align-self: center;
}
.stl-results__row--mastery       { --row-color: var(--stl-green); }
.stl-results__row--misconception { --row-color: var(--stl-red); }
.stl-results__row--lucky         { --row-color: var(--stl-yellow); }
.stl-results__row--gap           { --row-color: var(--stl-amber); }
.stl-results__row--fluke         { --row-color: rgba(var(--stl-red-rgb), 0.7); }
.stl-results__row--cold          { --row-color: var(--stl-text-faint); }

.stl-results__row-title {
  grid-column: 2;
  grid-row: 1;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.25;
  color: var(--stl-text);
}
.stl-results__row-hint {
  grid-column: 2;
  grid-row: 2;
  font-size: 11.5px;
  font-weight: 400;
  color: var(--stl-text-faint);
  line-height: 1.2;
  margin-top: 1px;
}
/* Stats group on the right end of each row — packs num + pct close
 * together so they read as one stat instead of two floating numbers. */
.stl-results__row-stats {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.stl-results__row-num {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 18px;
  line-height: 1;
  color: var(--stl-text);
  text-align: right;
}
/* Percentage rides in a small pill — soft tinted background, hairline
 * border, mono digits. Tinted by the row's --row-color so the pill
 * carries the signal hue. */
.stl-results__row-pct {
  font-family: var(--stl-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  font-weight: 500;
  color: var(--stl-text-muted);
  padding: 3px 8px;
  border-radius: var(--r-pill);
  background: rgba(var(--row-color-rgb, 255, 255, 255), 0.06);
  border: 1px solid rgba(var(--row-color-rgb, 255, 255, 255), 0.18);
  line-height: 1;
  min-width: 44px;
  text-align: center;
}
.stl-results__row--mastery       { --row-color-rgb: 74, 222, 128; }
.stl-results__row--misconception { --row-color-rgb: 251, 113, 133; }
.stl-results__row--lucky         { --row-color-rgb: 250, 204, 21; }
.stl-results__row--gap           { --row-color-rgb: 245, 158, 11; }
.stl-results__row--fluke         { --row-color-rgb: 251, 113, 133; }
.stl-results__row--cold          { --row-color-rgb: 148, 148, 160; }

/* Narrow widths — collapse the hint inline and tighten padding so
 * the row still reads cleanly on a phone. */
@media (max-width: 480px) {
  .stl-results__hero { padding: 18px 18px; }
  .stl-results__score-num { font-size: 2.2rem; }
  .stl-results__row {
    padding: 12px 14px;
    grid-template-columns: 1fr auto;
  }
  .stl-results__row-pct { display: none; }
  .stl-results__row-hint { display: none; }
}

/* =========================================================
 * RESULTS TOP — 2-column grid: merged overview on the left,
 * the per-topic radar in its own joined container on the
 * right. Slight overview-leaning split so the text-heavy
 * ledger gets a bit more room than the square chart. On
 * narrow widths the two stack to a single column.
 * ========================================================= */
.stl-results__top {
  display: grid;
  /* Reverted to the 0.85/1.15 split. With the canvas back to a true
   * square (aspect-ratio: 1 below) the polygon fills the column
   * exactly — no wasted horizontal margin around a non-square canvas
   * — so this column width is enough to land a visibly bigger polygon
   * than the prior 0.72/1.28 attempt (which left dead space on the
   * sides because the canvas was wider than tall). */
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: var(--gap-normal);
  /* Top-aligned columns so the overview card sits at its natural
   * compact height instead of stretching tall to match the square
   * chart tile. The left column then stacks overview + share/fresh
   * actions vertically, roughly balancing the chart's tall square. */
  align-items: start;
}

/* Left column wraps the overview card + the share/start-fresh actions
 * so they stack vertically directly under the overview, instead of
 * the share section spanning full width below the entire top grid.
 * gap:0 — the actions block owns its own top margin + divider line,
 * so layering an outer gap on top would double the spacing. */
.stl-results__left-col {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
}
.stl-results__top > .stl-radar-section { margin: 0; }

/* Joined container layout — back to its standard flex column with
 * the chart-zone keeping the base 16px padding + the chart-head as
 * a normal inline eyebrow above the canvas. The chart's "small
 * polygon in a big box" issue is Chart.js-side (the polygon's
 * radius is shrunk by pointLabels reservations even when labels
 * are off), so the fix lives in the chart config, not the chrome. */
.stl-results__top .stl-radar-section__joined {
  display: flex;
  flex-direction: column;
}
.stl-results__top .stl-radar-section__chart-zone {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Canvas host = the full chart-zone tile. Square (aspect-ratio: 1)
 * so the canvas matches the column width exactly. The polygon
 * (drawn by Chart.js as min(W,H)) fills the whole square. */
.stl-results__top .stl-radar-section__canvas-host {
  flex: 1 1 auto;
  width: 100%;
  max-width: none;
  max-height: none;
  aspect-ratio: 1 / 1;
}

@media (max-width: 720px) {
  .stl-results__top {
    grid-template-columns: 1fr;
    gap: var(--s-3);
  }
}

/* =========================================================
 * TWIN CTA ROW — Walk through review + Start new AI-tailored
 * test, side by side, both as rainbow pill outlines so they
 * read as equal-weight primary actions. Each takes an even
 * share of the row; on narrow widths they wrap to stacked.
 * ========================================================= */
.stl-results__cta-row {
  display: flex;
  gap: var(--s-3);
  flex-wrap: wrap;
  /* Hairline divider above the twin primary CTAs separates the
   * "Walk through review" / "Start new AI-tailored test" actions
   * from the 2-col overview+chart grid above. */
  border-top: 1px solid var(--stl-hairline);
  padding-top: var(--s-5);
  margin-top: var(--s-4);
}
.stl-results__cta-row > .stl-btn {
  /* Equal share of the row. min-width prevents either button from
   * compressing past usable size when one label is much longer. */
  flex: 1 1 220px;
  justify-content: center;
}

/* Three-tier action layout (Option A):
 *   1. Hero CTA      → Walk through review, full-width primary
 *   2. Share group   → eyebrow label + 3 buttons in a row
 *   3. Take another  → eyebrow label + 2 buttons in a row
 *
 * Eyebrow labels echo the typography system used elsewhere on the
 * results screen (Geist Mono, uppercase, subtle color), so the
 * groups feel like an extension of the existing language rather
 * than a new pattern. */
.stl-results__actions {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  /* No border-top here anymore — the divider belongs below the
   * whole 2-col top grid (on .stl-results__cta-row). The gap from
   * the parent .stl-results__left-col handles spacing between this
   * action row and the overview card above. */
  margin-top: var(--s-4);
}
.stl-results__group {
  display: grid;
  gap: 10px;
}
.stl-results__group-label {
  margin: 0;
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-results__group-row {
  display: flex;
  gap: var(--s-2);
  flex-wrap: wrap;
}
/* Each button in a group claims an even share of the row, with a
 * minimum width that prevents very narrow flex children. On phones
 * the rows stack to 1-col below the responsive breakpoint. */
.stl-results__group-row > .stl-btn,
.stl-results__group-row > .dc-btn {
  flex: 1 1 calc(33% - var(--s-2));
  min-width: 130px;
  justify-content: center;
}

/* Hero variant of the primary button — full width, beefier padding,
 * stamps the Walk-through CTA as the obvious next step. */
.stl-btn--hero {
  width: 100%;
  padding: 14px 20px;
  font-size: 15px;
  min-height: 52px;
}

/* Subdued (N) count appended to the hero label. Sits with a tabular
 * monospace voice so it reads as metadata, not part of the verb. */
.stl-btn__count {
  font-family: var(--stl-font-mono);
  font-weight: 500;
  font-size: 0.9em;
  margin-left: 4px;
  opacity: 0.78;
  letter-spacing: 0.02em;
}

/* --- review --------------------------------------------------------- */
.stl-review__choices {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--s-2);
}
.stl-review__choices li {
  padding: 10px var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  display: flex;
  gap: var(--s-3);
  align-items: center;
  font-size: 0.95rem;
}
.stl-review__choices li[data-state="correct"] {
  border-color: rgba(var(--stl-green-rgb), 0.45);
  background: rgba(var(--stl-green-rgb), 0.06);
}
.stl-review__choices li[data-state="picked-wrong"] {
  border-color: rgba(var(--stl-red-rgb), 0.45);
  background: rgba(var(--stl-red-rgb), 0.06);
}
.stl-review__choices li .stl-choice__letter { width: 22px; height: 22px; font-size: 11px; }
/* Right-aligned "✓ correct" / "✗ your answer" marker on review choices.
 * `margin-left: auto` in the flex row pushes the marker to the far edge
 * of the choice tile instead of letting it sit inline after the text. */
.stl-review__choices li .stl-choice__mark {
  margin-left: auto;
  padding-left: var(--s-3);
  font-size: 0.85rem;
  font-weight: 500;
  white-space: nowrap;
  color: var(--stl-text-subtle);
}
.stl-review__choices li[data-state="correct"] .stl-choice__mark {
  color: var(--stl-green-text);
}
.stl-review__choices li[data-state="picked-wrong"] .stl-choice__mark {
  color: var(--stl-red-text);
}
.stl-review__explain {
  padding: var(--s-4);
  border-radius: var(--r-md);
  background: var(--stl-bg);
  border: 1px solid var(--stl-hairline);
}
.stl-review__explain p { margin: var(--s-2) 0 0; line-height: 1.6; color: var(--stl-text); }
.stl-review__nav {
  display: flex;
  justify-content: space-between;
  gap: var(--s-2);
}

/* --- buttons -------------------------------------------------------- */
.stl-btn,
.dc-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 9px 16px;
  border-radius: 9px;
  font-family: inherit;
  font-weight: 500;
  font-size: 13.5px;
  letter-spacing: -0.005em;
  border: 1px solid var(--stl-hairline-hi);
  background: var(--stl-surface);
  color: var(--stl-text);
  cursor: pointer;
  text-decoration: none;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
  /* All transitions ride the springy curve so hover feels physical.
   * Box-shadow is included so the lavender hairline halo can fade in
   * with the rest of the state shift. */
  transition: background  var(--dur-base) var(--ease-spring),
              border-color var(--dur-base) var(--ease-spring),
              color       var(--dur-base) var(--ease-spring),
              transform   var(--dur-fast) var(--ease-spring),
              box-shadow  var(--dur-base) var(--ease-spring),
              filter      var(--dur-base) var(--ease-spring);
}
.stl-btn:hover,
.dc-btn:hover {
  /* Lift + lavender hairline halo. Subtle, but tells you the button is
   * "live" without competing with the primary CTA's stronger glow. */
  background: var(--stl-surface-2);
  border-color: var(--stl-accent-line);
  transform: translateY(-1px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 8px 18px -8px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.18);
}
.stl-btn:active,
.dc-btn:active {
  /* Quick rebound — feels like a physical press. */
  transform: translateY(0.5px);
  transition-duration: var(--dur-fast);
}
.stl-btn:focus-visible,
.dc-btn:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
.stl-btn:disabled,
.dc-btn:disabled {
  opacity: 0.45; cursor: not-allowed; transform: none; filter: none;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}

/* The author display rules above (inline-flex) win over the user-agent
 * `[hidden] { display: none }`, so any .stl-btn carrying the hidden
 * attribute would still render. Restore the expected behavior. */
.stl-btn[hidden],
.dc-btn[hidden] { display: none; }

/* hover-on-touch override — kill the hover paint on devices without a
 * pointer so taps don't leave the lifted state stuck. */
@media (hover: none) {
  .stl-btn:hover,
  .dc-btn:hover {
    transform: none;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
    border-color: var(--stl-hairline-hi);
    background: var(--stl-surface);
  }
}

/* primary — border-only rainbow, rectangular.
 *
 * This was a FILLED rainbow gradient until 2026-08-05. It was retired
 * because the app had already migrated nearly every CTA to the
 * border-only treatment (--pill-rainbow), and the dozen filled buttons
 * left behind (admin modal saves, the splash CTA, the resume card) read
 * as a louder, older system sitting right next to them.
 *
 * The treatment is now identical to --pill-rainbow with one deliberate
 * difference: the radius stays the base 9px RECTANGLE instead of going
 * full pill. Pills are reserved for the hero and navbar CTAs; form-level
 * actions stay rectangular.
 *
 * Implementation is the padding-box / border-box double-background
 * trick used everywhere else in this file. The 1.5px transparent border
 * carries the rainbow; the padding-box layer is an opaque fill that
 * hides the gradient everywhere except the border. #15161d dark,
 * #ffffff light (see the light-mode swap near the top of this file).
 *
 * --rainbow-outline rides along as an alias so the older class name
 * resolves to exactly the same thing. */
.stl-btn--primary,
.stl-btn--rainbow-outline {
  background:
    linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
    var(--stl-rainbow) border-box;
  /* 1.5px so the gradient border reads at small sizes — a 1px
   * four-stop gradient turns to mush on a 9px radius. */
  border: 1.5px solid transparent;
  border-radius: 9px;
  color: var(--stl-text);
  font-weight: 600;
  /* The border went 1px → 1.5px, so shave half a pixel off the
   * vertical padding and the button keeps the same height as the
   * ghost/secondary buttons it sits beside. */
  padding: 9.5px 18px;
  box-shadow: none;
  /* Force a separate GPU layer. Gradient borders on rounded corners
   * can show subpixel-render artifacts when composited against a
   * parent backdrop-filter (the navbar uses one); promoting the
   * button to its own layer with translate3d eliminates them. */
  transform: translate3d(0, 0, 0);
}
.stl-btn--primary:hover,
.stl-btn--rainbow-outline:hover {
  /* Lift the inside one shade so hover registers without touching
   * the gradient identity. Same values as --pill-rainbow. */
  background:
    linear-gradient(var(--stl-rainbow-fill-hover), var(--stl-rainbow-fill-hover)) padding-box,
    var(--stl-rainbow) border-box;
  /* translate3d keeps the button on the GPU layer through the hover
   * lift (translateY alone would force a re-rasterize and the corners
   * could pop). */
  transform: translate3d(0, -1px, 0);
  box-shadow: 0 10px 24px -10px rgba(var(--stl-rainbow-mid-rgb), 0.45);
}
.stl-btn--primary:active,
.stl-btn--rainbow-outline:active {
  transform: translate3d(0, 0.5px, 0);
  box-shadow: 0 4px 10px -4px rgba(var(--stl-rainbow-mid-rgb), 0.3);
}
@media (hover: none) {
  .stl-btn--primary:hover,
  .stl-btn--rainbow-outline:hover {
    transform: none;
    box-shadow: none;
    background:
      linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
      var(--stl-rainbow) border-box;
  }
}

/* outline — same gradient as primary, but only on the border. Dark
 * interior + white text. Used for the navbar "New test" CTA where
 * the filled primary felt too loud sitting next to the text nav
 * tabs; the outline reads as "action" without dominating the bar.
 *
 * Implementation: padding-box / border-box double-background trick
 * (same pattern as the "All" history chip + the active nav-icon
 * chip). The 1.5px transparent border carries the rainbow; the
 * padding-box layer is an opaque dark surface that hides the
 * gradient everywhere except the border. */
.stl-btn--outline {
  background:
    linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
    var(--stl-rainbow) border-box;
  border: 1.5px solid transparent;
  color: var(--stl-text);
  font-weight: 600;
  padding: 10px 18px;
  box-shadow: none;
  transform: translate3d(0, 0, 0);
}
.stl-btn--outline:hover {
  /* Subtle lift + tint the inside one shade lighter so hover reads
   * without changing the gradient identity. */
  background:
    linear-gradient(var(--stl-rainbow-fill-hover), var(--stl-rainbow-fill-hover)) padding-box,
    var(--stl-rainbow) border-box;
  transform: translate3d(0, -1px, 0);
  box-shadow:
    0 10px 24px -10px rgba(var(--stl-rainbow-mid-rgb), 0.5);
}
.stl-btn--outline:active {
  transform: translate3d(0, 0.5px, 0);
  box-shadow:
    0 4px 10px -4px rgba(var(--stl-rainbow-mid-rgb), 0.3);
}
@media (hover: none) {
  .stl-btn--outline:hover {
    transform: none;
    box-shadow: none;
    background:
      linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
      var(--stl-rainbow) border-box;
  }
}

/* ============ pill-rainbow ============
 * Border-only gradient pill. Used by the navbar "New test" CTA AND
 * (with a hero-sized override below) the full-width "Start new
 * AI-tailored test" hero CTA. Same padding-box / border-box trick
 * as the "All" history chip — the 1.5px transparent border carries
 * the rainbow; the inside is the dark navbar-effective color so the
 * gradient only shows on the perimeter. Text + icon white. */
.stl-btn--pill-rainbow {
  /* Border-only gradient pill — same padding-box / border-box trick
   * as the "All" history chip but with full pill radius. The 1.5px
   * transparent border carries the rainbow; the inside is the dark
   * navbar-effective color (#15161d) so the gradient only shows on
   * the perimeter. Text + icon white for clean contrast. */
  background:
    linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
    var(--stl-rainbow) border-box;
  color: var(--stl-text);
  border: 1.5px solid transparent;
  font-weight: 600;
  padding: 6px 14px 6px 12px;
  font-size: 12.5px;
  border-radius: var(--r-pill);
  box-shadow: none;
  min-height: 0;
  transform: translate3d(0, 0, 0);
}
.stl-btn--pill-rainbow:hover {
  /* Lift the inside one shade so hover registers without changing
   * the gradient identity. */
  background:
    linear-gradient(var(--stl-rainbow-fill-hover), var(--stl-rainbow-fill-hover)) padding-box,
    var(--stl-rainbow) border-box;
  transform: translate3d(0, -1px, 0);
  box-shadow: 0 8px 20px -10px rgba(var(--stl-rainbow-mid-rgb), 0.45);
}
/* Light-mode override — hardcoded #15161d interior is dark on dark on
 * purpose in dark mode, but in light mode it leaves the button as a
 * black pill against white with dark "New test" text invisible
 * inside. Swap the padding-box fill to white + use the light-mode
 * text color so contrast comes back. Rainbow border stays. */
:root[data-theme="light"] .stl-btn--pill-rainbow {
  background:
    linear-gradient(#ffffff, #ffffff) padding-box,
    var(--stl-rainbow) border-box;
  color: var(--stl-text);
}
/* .stl-btn--rainbow-outline used to be defined here as the rectangular
 * sibling of --pill-rainbow. As of 2026-08-05 --primary IS that
 * treatment, so the class is folded into the --primary selector group
 * above and kept only as an alias. Defining it again here would win on
 * source order and quietly reintroduce the old --r-md radius and
 * --stl-bg-2 fill, so it stays deleted. */

:root[data-theme="light"] .stl-btn--pill-rainbow:hover {
  background:
    linear-gradient(#f6f7fb, #f6f7fb) padding-box,
    var(--stl-rainbow) border-box;
  box-shadow: 0 8px 20px -10px rgba(var(--stl-rainbow-mid-rgb), 0.35);
}
/* Hero-size override — when --pill-rainbow lands on the full-width
 * AI-tailored CTA the navbar's compact sizing isn't right; bump
 * padding + font back up to the original primary-CTA scale so the
 * button reads as the page's hero action. */
.stl-radar__cta.stl-btn--pill-rainbow {
  padding: 13px 22px;
  font-size: 14px;
  /* Pure pill at hero scale would be overly stadium-like; nudge
   * back to a comfortable lozenge radius. */
  border-radius: var(--r-pill);
}
.stl-radar__cta.stl-btn--pill-rainbow:hover {
  /* Larger hero-scale shadow to match the button's increased weight. */
  box-shadow: 0 14px 32px -12px rgba(var(--stl-rainbow-mid-rgb), 0.5);
}

/* ghost — quiet by default, lavender wash on hover (Trench-style).
 * No border (so the wash hugs the text), color promotes to text-bright. */
/* ── NEUTRAL secondary + PILL shape ─────────────────────────────────────
 *
 * STYLEGUIDE.md called `--outline` "secondary", but --outline carries the
 * rainbow gradient border, and --ghost is borderless (a tertiary). So the
 * system had no NEUTRAL secondary: a real button, clearly clickable, that
 * yields visual priority to a brand-coloured primary beside it. Josh asked
 * for exactly that in the hero demo's action pair (2026-08-18) — "secondary
 * without the gradient" — so it is built once here rather than as a
 * one-off, per Rule #4.
 *
 * `--pill` is a SHAPE modifier only (radius, nothing else) so it composes
 * with any variant. --pill-rainbow already bakes the pill radius in; this is
 * for the variants that do not.
 */
.stl-btn--neutral {
  background: var(--stl-surface-2);
  border: 1.5px solid var(--stl-hairline-hi);
  color: var(--stl-text);
  font-weight: 600;
  padding: 9.5px 18px;
  box-shadow: none;
  transform: translate3d(0, 0, 0);
}
.stl-btn--neutral:hover {
  background: var(--stl-surface-3);
  border-color: var(--stl-text-subtle);
  transform: translate3d(0, -1px, 0);
}
.stl-btn--neutral:active { transform: translate3d(0, 0.5px, 0); }
@media (hover: none) {
  .stl-btn--neutral:hover {
    background: var(--stl-surface-2);
    border-color: var(--stl-hairline-hi);
    transform: none;
  }
}
.stl-btn--pill { border-radius: var(--r-pill); }

.stl-btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--stl-text-muted);
  box-shadow: none;
}
.stl-btn--ghost:hover {
  background: var(--stl-accent-soft);
  color: var(--stl-text);
  border-color: transparent;
  box-shadow: none;
  transform: none;
}
/* ADMIN nav tab — no special coloring. Inherits the same muted text
 * + gradient-border active treatment as the History and Analysis
 * tabs so the three nav links read as one consistent system. The
 * old green tint + green hover background was removed once the tab
 * moved into the shared .stl-header__nav-link group. */
@media (hover: none) {
  .stl-btn--ghost:hover { background: transparent; color: var(--stl-text-muted); }
}

/* arrow modifier — appends a → that nudges right and brightens on hover.
 * Apply by adding `stl-btn--arrow` to any .stl-btn. Pure CSS — no HTML
 * change to the label needed. */
.stl-btn--arrow::after {
  content: "→";
  display: inline-block;
  margin-left: 8px;
  font-weight: 500;
  opacity: 0.78;
  transform: translateX(-2px);
  transition: transform var(--dur-base) var(--ease-spring),
              opacity   var(--dur-base) var(--ease-spring);
}
.stl-btn--arrow:hover::after {
  opacity: 1;
  transform: translateX(2px);
}

/* Dev-only button (the "Test" affordance in the header that fast-
 * forwards to populated results). Visually marked as a developer
 * tool — mono font, slight red tint, dashed border — so nobody
 * mistakes it for production UI. JS hides it on non-local hostnames
 * and the click handler also feature-checks before doing anything,
 * so even if it leaks into production it no-ops. */
.stl-btn--dev {
  background: rgba(var(--stl-red-rgb), 0.06);
  border: 1px dashed rgba(var(--stl-red-rgb), 0.4);
  color: rgba(var(--stl-red-rgb), 0.85);
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 6px 12px;
  box-shadow: none;
}
.stl-btn--dev:hover {
  background: rgba(var(--stl-red-rgb), 0.12);
  border-color: rgba(var(--stl-red-rgb), 0.7);
  color: var(--stl-red);
}

/* small icon prefix used inside .stl-btn — currently the share button.
 * Sits in front of the label with a subtle gap and slightly larger
 * weight so it reads as part of the affordance, not decoration. */
.stl-btn__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-right: 2px;
  font-weight: 600;
  font-size: 1.1em;
  line-height: 1;
  letter-spacing: 0;
}

/* ============== inline link styles (Trench-inspired) ============== *
 * Use .stl-link wherever a text link sits in body copy. Underline
 * scales in from the left on hover (1px, lavender). .stl-link--arrow
 * adds a trailing → that nudges right.
 */
.stl-link {
  position: relative;
  display: inline;
  color: var(--stl-text);
  text-decoration: none;
  padding: 0 0 1px 0;
  /* Reset native button chrome — `.stl-link` is sometimes applied to
   * a <button> (e.g., the "choose a file" button inside a dropzone)
   * and we don't want the user-agent's button background/border
   * obscuring the text. iOS Safari is especially aggressive: it adds
   * border-radius, a tap-highlight color, and a subtle inner shadow
   * even when `appearance: none` is set, so we override each one
   * explicitly here. */
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  margin: 0;
  font: inherit;
  text-align: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-base) var(--ease-spring);
}
.stl-link::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-slow) var(--ease-spring);
}
.stl-link:hover { color: var(--stl-accent); }
.stl-link:hover::after { transform: scaleX(1); }
.stl-link--accent       { color: var(--stl-accent); }
.stl-link--accent:hover { color: var(--stl-accent-2); }

/* utility */
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0);
  white-space: nowrap; border: 0;
}

/* ============== history screen =====================================
 * Past attempts as a vertical ledger. Each row is a button (the body)
 * with a trailing action cluster. Newest first; chronological cadence
 * carried by the relative date label ("Just now", "12 min ago", etc.).
 * The colored dot stripe in each row is a glance-able 4-bucket
 * summary using the same green/yellow/red palette as the bento.
 */
/* Single source of vertical rhythm for this card. Children separate
 * by --gap-normal (16px). The chip strip and radar section both used
 * to add their own margin on top of this gap, which compounded to
 * ~40px between them; those margins are now removed so the parent
 * gap is the only authority. */
/* align-content: start is load-bearing. The card IS the grid (the same
 * element carries .stl-card and .stl-history), and it gets stretched to
 * the screen's height. With the default `normal` (= stretch) the spare
 * height is dealt out to the auto rows, so every child grows: the chip
 * row became tall ovals, the dashed empty-state a full-height slab, and
 * on the Analysis page's first paint — radar row still hidden — the AI
 * CTA inflated into a giant button (Josh 2026-08-11). Packing the rows
 * to the top makes each one its natural height and leaves the slack at
 * the bottom of the card where it belongs. */
.stl-history { display: grid; gap: var(--gap-normal); align-content: start; }
/* Head row: text block grows to fill, bulk-action toolbar pinned right.
 * Wraps below the text on narrow screens so the buttons don't push the
 * title to half-width. */
.stl-history__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap-tight);
  flex-wrap: wrap;
}
.stl-history__head-text { display: grid; gap: var(--gap-hug); min-width: 0; flex: 1 1 320px; }
.stl-history__bulk {
  display: inline-flex;
  gap: var(--s-2);
  flex-wrap: wrap;
  align-items: center;
  /* Nudge the actions up so they baseline with the H1, not the sub. */
  margin-top: 4px;
}

/* History test-type filter chips. Pill-style toggle group; the active
 * chip picks up an accent border + tinted background. Hidden when
 * the user has only attempted one test type. */
.stl-history__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-hug);                /* 8px between chips */
  /* No margin-bottom — the parent .stl-history grid gap handles
   * separation between the chip row and the next subsection. Extra
   * margin here would compound with that gap. */
}
/* The display: flex above wins against the user-agent
 * `[hidden] { display: none }`, so the empty/hidden chip row would
 * otherwise still occupy a flex slot and leave a phantom gap.
 * Restore the expected hide. */
.stl-history__chips[hidden] { display: none; }
.stl-history__chip {
  appearance: none; -webkit-appearance: none;
  font: inherit; color: var(--stl-text-muted);
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px;
  /* Border width matches the active "All" chip's 1.5px gradient ring
   * (.stl-history__chip[data-history-filter="all"].is-active). When the
   * default was 1px the row would shift by 1px the instant you switched
   * to All — the chip got taller. Holding both at 1.5px keeps every
   * chip the same outer size regardless of which one is active. */
  border: 1.5px solid var(--stl-hairline);
  border-radius: var(--r-pill);
  background: transparent;
  font-size: 13px;
  font-weight: 500;
  /* --chip-tint is set inline per chip to that test's RGB triplet
   * (e.g. PSAT → "96, 165, 250"). The "All" chip leaves it unset
   * and falls back to the global accent so it stays neutral. Every
   * tint reference below pulls from --chip-tint first, then the
   * global --stl-tint as a fallback. */
  transition: border-color var(--dur-base) ease,
              background var(--dur-base) ease,
              color var(--dur-base) ease;
}
.stl-history__chip:hover {
  color: var(--stl-text);
  /* Hint at the chip's color on hover even in the inactive state so
   * users can preview each test's identity before clicking. */
  border-color: rgba(var(--chip-tint, var(--stl-tint, var(--stl-rainbow-mid-rgb))), 0.4);
  background: rgba(var(--chip-tint, var(--stl-tint, var(--stl-rainbow-mid-rgb))), 0.05);
}
.stl-history__chip:focus-visible {
  outline: 2px solid rgba(var(--chip-tint, var(--stl-tint, var(--stl-rainbow-mid-rgb))), 0.9);
  outline-offset: 2px;
}
.stl-history__chip.is-active {
  color: var(--stl-text);
  border-color: rgba(var(--chip-tint, var(--stl-rainbow-mid-rgb)), 0.6);
  background: rgba(var(--chip-tint, var(--stl-rainbow-mid-rgb)), 0.14);
  box-shadow: 0 6px 18px -6px rgba(var(--chip-tint, var(--stl-rainbow-mid-rgb)), 0.35);
}
/* "All" chip (no test selected → no --chip-tint set) uses the actual
 * G2 gradient as a 1.5px border via a padding-box / border-box trick,
 * with the dark surface showing through inside. The base .is-active
 * rule's tinted fill + indigo box-shadow are explicitly cleared so
 * the chip reads as a clean gradient OUTLINE rather than a filled
 * pill — matches the rainbow-gradients.html preview. */
.stl-history__chip[data-history-filter="all"].is-active {
  border: 1.5px solid transparent;
  /* The padding-box layer must be OPAQUE so it actually hides the
   * gradient underneath. --stl-surface is rgba(255,255,255,0.035)
   * — almost transparent — which was letting the rainbow show
   * through the whole chip. Using --stl-bg (the solid page color)
   * + a tiny lightness bump to match the card's rendered look
   * blocks the gradient cleanly so only the 1.5px border shows it. */
  background:
    linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
    var(--stl-rainbow) border-box;
  box-shadow: none;
}
/* And its count badge gets the FULL gradient fill (not the soft
 * tinted version used for per-test chips), with dark text on top. */
.stl-history__chip[data-history-filter="all"].is-active .stl-history__chip-n {
  background: var(--stl-rainbow);
  color: var(--stl-bg);
  font-weight: 600;
}
.stl-history__chip-n {
  display: inline-grid; place-items: center;
  min-width: 20px; height: 18px;
  padding: 0 6px;
  border-radius: 9px;
  background: var(--stl-surface-2);
  font-size: 11px;
  font-family: var(--stl-font-mono);
  color: var(--stl-text-subtle);
  font-weight: 500;
}
.stl-history__chip.is-active .stl-history__chip-n {
  background: rgba(var(--chip-tint, var(--stl-tint, var(--stl-rainbow-mid-rgb))), 0.25);
  color: var(--stl-text);
}
.stl-history__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}
.stl-history__row {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: stretch;
  gap: var(--s-2);
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  overflow: hidden;
  transition: border-color var(--dur-base) var(--ease-spring),
              background   var(--dur-base) var(--ease-spring),
              transform    var(--dur-fast) var(--ease-spring),
              box-shadow   var(--dur-base) var(--ease-spring);
}
/* Left-edge accent grows in on hover — same pattern as .stl-choice
 * but at row-scale. The bar's color is keyed to the row's test type
 * via --row-tint (set inline by renderHistory from the attempt's
 * testType → TEST_TYPES.tint). Falls back to the global accent
 * when no tint is provided. Tells you at a glance which test the
 * row belongs to AND that the row is the tap target. */
.stl-history__row::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: rgb(var(--row-tint, var(--stl-tint, var(--stl-rainbow-mid-rgb))));
  transform: scaleY(0);
  transition: transform var(--dur-base) var(--ease-spring);
}
.stl-history__row:hover {
  /* Border and lift glow also pick up the row's test tint so the
   * whole hover state reads as that specific test's color. */
  border-color: rgba(var(--row-tint, var(--stl-tint, var(--stl-rainbow-mid-rgb))), 0.5);
  background: var(--stl-surface-2);
  transform: translateY(-1px);
  box-shadow: 0 8px 22px -10px rgba(var(--row-tint, var(--stl-tint, var(--stl-rainbow-mid-rgb))), 0.32);
}
.stl-history__row:hover::before { transform: scaleY(1); }
.stl-history__row.is-synthetic { border-style: dashed; opacity: 0.85; }
@media (hover: none) {
  .stl-history__row:hover {
    transform: none; box-shadow: none;
    border-color: var(--stl-hairline); background: var(--stl-surface);
  }
  .stl-history__row:hover::before { transform: scaleY(0); }
}

.stl-history__open {
  appearance: none; -webkit-appearance: none;
  font: inherit; color: inherit;
  /* Columns: test-badge | date | target | score | dots-bar | time
   * Score + time columns are pinned to fixed widths so a wider value
   * ("14/30" vs "5/30", "1:13:27" vs "0:39") never expands the
   * column track — which would otherwise push the whole row outward
   * because the row's 1fr cell grows to fit. min-width: 0 on the
   * button itself caps that growth even if a column overflows. */
  display: grid;
  grid-template-columns: auto minmax(7em, 9em) minmax(5em, auto) 4.25em 1fr 4.75em;
  align-items: center;
  gap: var(--s-3);
  padding: 12px var(--s-4);
  background: transparent;
  border: 0;
  cursor: pointer;
  text-align: left;
  border-radius: var(--r-md);
  min-height: 56px;
  min-width: 0;
}
.stl-history__test {
  display: inline-flex;
  align-items: center;
  /* The badge itself sets its own padding/border — the wrapper just
   * positions it inside the grid cell. */
}
.stl-history__open:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}

.stl-history__date {
  font-family: var(--stl-font-mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--stl-text-muted);
  white-space: nowrap;
}
.stl-history__synth {
  font-style: normal;
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: var(--r-pill);
  background: rgba(var(--stl-red-rgb), 0.08);
  color: rgba(var(--stl-red-rgb), 0.9);
  font-size: 9.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.stl-history__target {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-history__score {
  font-size: 1.15rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--stl-text);
  font-variant-numeric: tabular-nums;
}
.stl-history__score small {
  font-weight: 500;
  font-size: 0.85em;
  color: var(--stl-text-subtle);
}

/* The 3-dot stripe is a flex row of weighted segments — true mastery
 * (green), lucky (yellow), to-review (red). Width per segment scales
 * with the count via the --w custom prop; min-width keeps zero counts
 * as a thin pill instead of disappearing. */
.stl-history__dots {
  display: flex;
  gap: 3px;
  height: 6px;
  width: 100%;
  min-width: 64px;
  max-width: 180px;
}
.stl-history__dot {
  flex: var(--w, 0) 0 0;
  min-width: 6px;
  height: 100%;
  border-radius: var(--r-pill);
  opacity: 0.85;
}
.stl-history__dot--g { background: var(--stl-green); }
.stl-history__dot--y { background: var(--stl-yellow); }
.stl-history__dot--r { background: var(--stl-red); }

.stl-history__time {
  font-family: var(--stl-font-mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--stl-text-subtle);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.stl-history__actions {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 6px var(--s-3) 6px 0;
}
.stl-history__action {
  appearance: none; -webkit-appearance: none;
  font: inherit; cursor: pointer;
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  background: transparent;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-sm);
  padding: 6px 8px;
  min-width: 36px;
  transition: color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}
.stl-history__action {
  transition: color var(--dur-base) var(--ease-spring),
              border-color var(--dur-base) var(--ease-spring),
              background var(--dur-base) var(--ease-spring),
              box-shadow var(--dur-base) var(--ease-spring),
              transform var(--dur-fast) var(--ease-spring);
}
.stl-history__action:hover {
  color: var(--stl-text);
  border-color: var(--stl-accent-line);
  background: var(--stl-accent-soft);
  box-shadow: 0 4px 12px -6px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.35);
  transform: translateY(-1px);
}
.stl-history__action:active { transform: translateY(0); }
.stl-history__action:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
.stl-history__action--danger {
  font-size: 16px;
  line-height: 1;
  padding: 4px 8px;
}
/* Icon variant — used for the Copy-link button (SVG icon inside)
 * and re-used by the × danger button. Square-ish; the SVG inside
 * centers via flex. Same outer height as the text JSON button so
 * the action row reads as evenly-sized chips across all rows
 * regardless of which buttons render. */
.stl-history__action--icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  min-width: 0;
}
.stl-history__action--icon svg {
  display: block;
}
/* Pin the JSON text button to the same height so admin + non-admin
 * rows line up identically when JSON is visible. */
.stl-history__action {
  height: 32px;
  line-height: 1;
  padding: 0 10px;
}
.stl-history__action--danger:hover {
  color: var(--stl-red);
  border-color: rgba(var(--stl-red-rgb), 0.6);
  background: rgba(var(--stl-red-rgb), 0.08);
  box-shadow: 0 4px 12px -6px rgba(var(--stl-red-rgb), 0.4);
}
/* Archive action — recessive at rest, picks up a muted slate hover so
 * it reads distinctly from the primary Publish/Unpublish toggle and
 * the destructive red of --danger. Archive is reversible, so the
 * affordance shouldn't look scary. */
.stl-history__action--archive {
  color: var(--stl-text-subtle);
}
.stl-history__action--archive:hover {
  color: var(--stl-text);
  border-color: var(--stl-hairline-hi);
  background: rgba(148, 163, 184, 0.08);
  box-shadow: 0 4px 12px -6px rgba(148, 163, 184, 0.35);
}
@media (hover: none) {
  .stl-history__action:hover { transform: none; box-shadow: none; }
}

.stl-history__empty {
  padding: var(--s-6) var(--s-4);
  text-align: center;
  border: 1px dashed var(--stl-hairline);
  border-radius: var(--r-md);
}

.stl-history__footer {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  align-items: center;
  justify-content: space-between;
  padding-top: var(--s-3);
  border-top: 1px solid var(--stl-hairline);
}
.stl-history__bulk { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* Phone tweaks — collapse the 5-col row into 2 stacked rows so there's
 * room for everything without horizontal squeeze. The test badge tucks
 * into the top-left where the date used to start; date pushes right. */
@media (max-width: 560px) {
  .stl-history__open {
    grid-template-columns: auto 1fr auto auto;
    grid-template-areas:
      "test date target time"
      "score dots dots dots";
    gap: 8px var(--s-3);
    padding: 12px var(--s-3);
  }
  .stl-history__test   { grid-area: test; }
  .stl-history__date   { grid-area: date; }
  .stl-history__target { grid-area: target; }
  .stl-history__time   { grid-area: time; }
  .stl-history__score  { grid-area: score; }
  .stl-history__dots   { grid-area: dots; min-width: 0; max-width: none; }
  .stl-history__actions { padding: 8px var(--s-3) 8px 0; }
  .stl-history__action  { padding: 6px 7px; min-width: 32px; }
}

/* ============== admin tool =====================================
 * Database admin: page-shaped (no hero card), wider canvas. Three
 * stacked sections — page head, KPI strip, filter+table panel —
 * with their own treatment instead of being crammed into a single
 * card like quiz/results. Role-gated (admin only).
 */
.stl-screen--admin {
  /* Override the default grid gap for this screen — we want clear
   * vertical sections rather than a uniform card-stack rhythm. */
  display: grid;
  gap: var(--s-5);
}

/* --- page head: title + back --- */
.stl-admin__page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--stl-hairline);
}
.stl-admin__page-titles { display: grid; gap: 4px; }
.stl-admin__h1 {
  margin: 0;
  font-family: var(--stl-font-display);
  font-size: clamp(1.5rem, 2.4vw, 2rem);
  letter-spacing: -0.02em;
  font-weight: 600;
  line-height: 1.05;
  color: var(--stl-text);
}
/* Tests-panel section heading — same display font as h1 for cohesion. */
.stl-admin__h2 {
  margin: 0 0 var(--s-2);
  font-family: var(--stl-font-display);
  font-size: clamp(1.25rem, 2vw, 1.5rem);
  letter-spacing: -0.015em;
  font-weight: 600;
  line-height: 1.15;
  color: var(--stl-text);
}
.stl-admin__page-actions { display: flex; gap: var(--s-2); }

/* --- KPI strip: 6 squat tiles in a row --- */
.stl-admin__kpis {
  display: grid;
  /* 7 tiles on one line. minmax(0, 1fr) lets each cell shrink past
   * its content min-width, which would otherwise force wrapping
   * because the longer labels (HUMAN-CURATED, NEEDS REVIEW) are wider
   * than the 8-col fallback. The label rule below clips with ellipsis
   * before that becomes a problem. */
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--s-2);
}
.stl-admin__kpi {
  padding: 12px 14px;
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  display: grid;
  gap: 2px;
  position: relative;
  overflow: hidden;
}
/* Interactive variant — only the Questions-tab tiles are <button>s.
 * The Users-tab strip keeps <div>s and stays static, so we scope
 * cursor/hover/active to the button selector. */
button.stl-admin__kpi {
  appearance: none; -webkit-appearance: none;
  font: inherit; color: inherit; text-align: left;
  cursor: pointer;
  width: 100%;
  transition:
    transform var(--dur-fast) var(--ease-spring),
    border-color var(--dur-base) var(--ease-spring),
    box-shadow var(--dur-base) var(--ease-spring),
    background var(--dur-base) var(--ease-spring);
}
button.stl-admin__kpi:hover {
  transform: translateY(-1px);
  border-color: rgba(var(--kpi-tint, 148, 163, 184), 0.5);
  box-shadow: 0 6px 16px -8px rgba(var(--kpi-tint, 148, 163, 184), 0.4);
}
button.stl-admin__kpi:active { transform: translateY(0); }
button.stl-admin__kpi:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
/* Active tile = the filter currently applied. Border + soft halo in
 * the tile's own tint reads as "you are here" without needing a
 * separate label or check icon. */
button.stl-admin__kpi[aria-pressed="true"] {
  border-color: rgba(var(--kpi-tint, 148, 163, 184), 0.7);
  box-shadow: 0 6px 16px -8px rgba(var(--kpi-tint, 148, 163, 184), 0.45);
}
button.stl-admin__kpi[aria-pressed="true"]::before {
  background: linear-gradient(135deg, rgba(var(--kpi-tint, 148, 163, 184), 0.16), transparent 70%);
}
.stl-admin__kpi::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, rgba(var(--kpi-tint, 148, 163, 184), 0.08), transparent 60%);
  pointer-events: none;
}
.stl-admin__kpi-lbl {
  margin: 0;
  font-family: var(--stl-font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  position: relative;
  /* Long labels (HUMAN-CURATED, NEEDS REVIEW) clip with ellipsis
   * rather than wrap — keeps every tile a single line, same height. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stl-admin__kpi-num {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--stl-text);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  position: relative;
}
/* Per-tile tint via a custom property — green for live, amber for
 * needs-review, red for unpublished, BLUE for AI (the content/task
 * lane owns AI-generated), neutral slate for total/human/archived.
 * Re-toned 2026-08-10 off the legacy lavender. */
.stl-admin__kpi[data-kind="live"]         { --kpi-tint: 74, 222, 128; }
.stl-admin__kpi[data-kind="needs-review"] { --kpi-tint: 250, 204, 21; }
.stl-admin__kpi[data-kind="unpublished"]  { --kpi-tint: 251, 113, 133; }
/* Archived KPI deliberately uses neutral text-subtle (no chroma) so
 * the count reads as inert/parked, not a status to act on. */
.stl-admin__kpi[data-kind="archived"]     { --kpi-tint: 148, 163, 184; }
.stl-admin__kpi[data-kind="ai"]           { --kpi-tint: 96, 165, 250; }
.stl-admin__kpi[data-kind="live"] .stl-admin__kpi-num         { color: var(--stl-green-text); }
.stl-admin__kpi[data-kind="needs-review"] .stl-admin__kpi-num { color: var(--stl-yellow-text); }
.stl-admin__kpi[data-kind="unpublished"] .stl-admin__kpi-num  { color: var(--stl-red-text); }
.stl-admin__kpi[data-kind="archived"] .stl-admin__kpi-num     { color: var(--stl-text-subtle); }
.stl-admin__kpi[data-kind="ai"] .stl-admin__kpi-num           { color: var(--stl-blue); }
:root[data-theme="light"] .stl-admin__kpi[data-kind="ai"] .stl-admin__kpi-num { color: var(--stl-blue); }

/* ===== Total tile — generic-default styling using the brand rainbow ===== */
/* No --kpi-tint (it's the "all" view), so the per-kind tints don't
 * apply. Instead, the corner background shine uses --stl-rainbow-soft
 * (a faded version of the brand gradient — matches the way other
 * tiles use their own tint as a faded shine), and the selected state
 * uses the same outline-gradient treatment as the "All" history chip
 * and the active nav-icon chip. */
.stl-admin__kpi[data-kind="total"]::before {
  background: var(--stl-rainbow-soft);
  opacity: 0.6;            /* match other tiles' relative shine weight */
}
button.stl-admin__kpi[data-kind="total"]:hover {
  border-color: rgba(var(--stl-rainbow-mid-rgb), 0.5);
  box-shadow: 0 6px 16px -8px rgba(var(--stl-rainbow-mid-rgb), 0.4);
}
button.stl-admin__kpi[data-kind="total"]:focus-visible {
  outline: 2px solid rgba(var(--stl-rainbow-mid-rgb), 0.9);
}
button.stl-admin__kpi[data-kind="total"][aria-pressed="true"] {
  /* Gradient border via padding-box / border-box trick. #15161d
   * matches the card's rendered surface (--stl-surface itself is too
   * transparent to block the gradient — same pitfall as the "All"
   * history chip). */
  border: 1.5px solid transparent;
  background:
    linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
    var(--stl-rainbow) border-box;
  box-shadow: 0 6px 18px -8px rgba(var(--stl-rainbow-mid-rgb), 0.45);
}
button.stl-admin__kpi[data-kind="total"][aria-pressed="true"]::before {
  /* Brighten the shine slightly when selected so the rainbow reads
   * cleanly both as the border AND as the in-tile wash. */
  background: var(--stl-rainbow-soft);
  opacity: 1;
}

/* --- tab navigation (Users / Questions) ---
 * Underline-tab pattern from material/ant dashboards: lavender accent
 * underlines the active tab; muted text for inactive. The count pill
 * keeps the eye on top-line scale ("how many users do we have?")
 * even before drilling in. */
.stl-admin__tabs {
  display: flex;
  gap: var(--s-2);
  /* Vertical-center so the right-side actions group sits in line with
   * the tab labels instead of stretching to the full row height. */
  align-items: center;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--stl-hairline);
  padding-bottom: 0;
  margin-bottom: var(--s-3);
}
/* Right-side actions group inside the tabs nav. Hosts the buttons
 * that used to live in the removed .stl-admin__page-head row — the
 * tab-scoped CTA (e.g., "Add questions") and the always-on
 * "Back to start". margin-left: auto pushes the group to the
 * trailing edge so the tab labels keep the leading edge. */
.stl-admin__tabs-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  /* Lift off the bottom hairline so the buttons aren't sitting on it. */
  padding-bottom: 6px;
}
.stl-admin__tab {
  appearance: none; -webkit-appearance: none;
  background: transparent;
  border: 0;
  /* Gradient underline indicator lives on a pseudo-element pinned to
   * the tabs row's bottom hairline — matches the main navbar's
   * sliding-bar treatment so admin and primary nav share one visual
   * language for "active tab". The pseudo's gradient swaps in via
   * opacity on [aria-selected="true"]. */
  position: relative;
  padding: 12px 4px;
  margin-bottom: -1px;     /* overlap parent border */
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--stl-text-muted);
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-spring);
}
.stl-admin__tab::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 2px;
  background: var(--stl-rainbow);
  opacity: 0;
  transition: opacity var(--dur-base) ease;
  pointer-events: none;
}
.stl-admin__tab:hover { color: var(--stl-text); }
.stl-admin__tab:hover::after { opacity: 0.35; }
.stl-admin__tab[aria-selected="true"] { color: var(--stl-text); }
.stl-admin__tab[aria-selected="true"]::after { opacity: 1; }
.stl-admin__tab:focus-visible {
  outline: 2px solid rgba(var(--stl-rainbow-mid-rgb), 0.9);
  outline-offset: 2px;
  border-radius: 2px;
}
.stl-admin__tab + .stl-admin__tab { margin-left: var(--s-2); }
.stl-admin__tab-count {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--stl-text-faint);
  padding: 2px 8px;
  border-radius: var(--r-pill);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
}
/* Active tab's count badge — gradient border via padding-box /
 * border-box trick (same pattern as the "All" history chip), with
 * white text inside. */
.stl-admin__tab[aria-selected="true"] .stl-admin__tab-count {
  border: 1px solid transparent;
  background:
    linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
    var(--stl-rainbow) border-box;
  color: var(--stl-text);
}

/* Tab pane wrapper — only one visible at a time. */
.stl-admin__pane[hidden] { display: none; }
.stl-admin__pane {
  display: grid;
  gap: var(--s-5);
  /* Anchors .stl-admin__pane-veil. */
  position: relative;
}
/* Loading veil — covers a pane while its data is still in flight so we
 * never paint an empty table, a zeroed KPI strip, or "No questions to
 * show" as if that were the answer. Reuses .stl-boot-veil__spinner:
 * the app has one spinner, not two. Raised by showPaneVeil() in app.js
 * after a short delay so a warm pane never flashes it. */
.stl-admin__pane-veil {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: grid;
  place-items: center;
  /* Reads as the panel it is standing in for: same radius, same
   * hairline, same fill. --stl-surface is a translucent wash by design,
   * so it is layered over an opaque base rather than used alone —
   * half-painted rows underneath must not read through the veil as if
   * they were real content, but a flat --stl-bg slab does not match the
   * card it covers and lands as a hole punched in the page. */
  background:
    linear-gradient(var(--stl-surface), var(--stl-surface)),
    var(--stl-bg);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
}

/* The PANE reserves the height, not the veil.
 *
 * This used to be `min-height: 180px` on the veil itself, which is a
 * trap: the veil is inset:0 on the pane, so a min-height taller than
 * the pane made the veil overflow the box it was supposed to be
 * covering and paint over whatever came next on the page. On a nearly
 * empty Imports pane that meant a slab of black sitting on top of the
 * site footer. Reserving the space on the busy pane instead means the
 * veil is always exactly the size of what it hides.
 *
 * aria-busy is set and cleared by showPaneVeil() / hidePaneVeil() in
 * app.js, so the reservation lasts precisely as long as the veil does
 * and no pane carries dead height once its content lands. */
.stl-admin__pane[aria-busy="true"] { min-height: 220px; }
.stl-admin__pane-veil[hidden] { display: none; }

/* ─── Users table cells ─── */
.stl-admin__table tr.stl-users__row {
  cursor: pointer;
}
.stl-admin__table tr.stl-users__row:hover {
  background: var(--stl-surface-2);
}
.stl-users__user {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.stl-users__user-name {
  color: var(--stl-text);
  font-weight: 500;
  font-size: 13.5px;
}
.stl-users__user-id {
  color: var(--stl-text-faint);
  font-family: var(--stl-font-mono);
  font-size: 11px;
}
.stl-users__type {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 1px solid var(--stl-hairline);
  color: var(--stl-text-muted);
  background: var(--stl-surface-2);
}
.stl-users__type[data-type="admin"] {
  color: var(--stl-accent);
  border-color: var(--stl-accent-line);
  background: var(--stl-accent-soft);
}
.stl-users__type[data-type="user"] {
  color: var(--stl-green-text);
  border-color: rgba(var(--stl-green-rgb), 0.3);
  background: rgba(var(--stl-green-rgb), 0.08);
}
.stl-users__num,
.stl-users__pct {
  font-variant-numeric: tabular-nums;
  color: var(--stl-text);
}
.stl-users__lastseen {
  color: var(--stl-text-muted);
  font-size: 12.5px;
}

/* ─── Access panel (runtime admin / teacher grants) ───
 * Sits under the users table in the Users tab. Shares the panel, table
 * and filter-row chrome with everything else in Admin so the grant form
 * reads as part of the same surface rather than a bolted-on settings
 * screen. The only new pieces are the heading block, the notice line
 * that failed fetches write into, and the per-row revoke control. */
.stl-access__head {
  padding: var(--s-4) var(--s-4) 0;
  display: grid;
  gap: 4px;
}
.stl-access__title {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--stl-text);
  letter-spacing: -0.01em;
}
.stl-access__sub {
  margin: 0;
  font-size: 12.5px;
  color: var(--stl-text-muted);
  max-width: 60ch;
}

/* One line, three moods. Errors are the default because that is what a
 * failed fetch writes; data-tone flips it for the other two. */
.stl-access__notice {
  margin: var(--s-3) var(--s-4) 0;
  padding: 9px 12px;
  border-radius: var(--r-md);
  font-size: 12.5px;
  line-height: 1.45;
  border: 1px solid rgba(var(--stl-red-rgb), 0.4);
  background: rgba(var(--stl-red-rgb), 0.08);
  color: var(--stl-red-text);
}
.stl-access__notice[data-tone="info"] {
  border-color: var(--stl-hairline);
  background: var(--stl-surface-2);
  color: var(--stl-text-muted);
}
.stl-access__notice[data-tone="ok"] {
  border-color: rgba(var(--stl-green-rgb), 0.3);
  background: rgba(var(--stl-green-rgb), 0.08);
  color: var(--stl-green-text);
}

/* The grant form reuses .stl-admin__filters for its grid, but it is a
 * form rather than a filter strip, so it gets its own top rule instead
 * of the filter strip's bottom one when a notice sits above it. */
.stl-access__form { border-top: 0; }

.stl-access__email {
  font-family: var(--stl-font-mono);
  font-size: 12.5px;
  color: var(--stl-text);
}
.stl-access__note {
  display: block;
  color: var(--stl-text-faint);
  font-size: 11.5px;
  margin-top: 2px;
  max-width: var(--measure-body);
}
.stl-access__meta {
  color: var(--stl-text-muted);
  font-size: 12px;
}
.stl-access__th-actions,
.stl-access__actions { text-align: right; width: 1%; white-space: nowrap; }

/* Bootstrap rows carry no revoke button — the endpoint would 400 it —
 * so they get a label in its place explaining why the control is
 * missing. Same for your own row. */
.stl-access__locked {
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stl-text-faint);
}
.stl-access__row[data-busy="1"] { opacity: 0.5; pointer-events: none; }

/* ─── Activity panel (audit log reader) ───
 * Deliberately borrows the Access panel's heading, sub and notice
 * classes rather than cloning them: the two panels sit one above the
 * other in the same tab, and a second set of near-identical rules would
 * drift apart the first time either was touched. What is new here is
 * the filter grid (six controls instead of the default three), the
 * per-row typography, and the footer that has to hold a button. */
.stl-activity__form {
  /* Event · Result · Since · Person · Matching · Refresh. Person takes
   * the slack because an email address is the one field that can be
   * long; everything else holds a short fixed set of options. */
  grid-template-columns: 1.3fr 0.9fr 0.9fr 1.6fr 1fr auto;
}
@media (max-width: 900px) {
  .stl-activity__form { grid-template-columns: 1fr 1fr; }
}

/* Rows are dense and nothing in them is clickable, so the only job here
 * is keeping the eye on one line while it tracks across five columns. */
.stl-activity__row td { vertical-align: top; }
.stl-activity__row:hover td { background: var(--stl-surface-2); }

.stl-activity__when {
  color: var(--stl-text-muted);
  font-size: 12px;
  white-space: nowrap;
}
/* The absolute timestamp under the relative one. Both matter: "2 hours
 * ago" is how you read a list, the exact time is how you correlate it
 * with something else. */
.stl-activity__when-abs {
  display: block;
  color: var(--stl-text-faint);
  font-size: 10.5px;
  margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
.stl-activity__event { color: var(--stl-text); font-size: 12.5px; }
/* The raw action string, kept visible under the friendly label. A
 * reader who knows the codes should not have to guess which one a
 * label maps to, and a code with no label yet still reads sensibly. */
.stl-activity__code {
  display: block;
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  color: var(--stl-text-faint);
  margin-top: 2px;
}
.stl-activity__who {
  font-family: var(--stl-font-mono);
  font-size: 12px;
  color: var(--stl-text);
  word-break: break-all;
}
.stl-activity__target {
  display: block;
  color: var(--stl-text-muted);
  font-size: 11.5px;
  margin-top: 2px;
  word-break: break-all;
}
.stl-activity__detail {
  color: var(--stl-text-muted);
  font-size: 11.5px;
  max-width: 40ch;
}
.stl-activity__th-source,
.stl-activity__source {
  text-align: right;
  white-space: nowrap;
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  color: var(--stl-text-faint);
}

/* Result pill. Denied is the one worth catching from across the room,
 * so it is the only loud colour; ok stays quiet on purpose. */
.stl-activity__outcome {
  display: inline-block;
  font-family: var(--stl-font-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: var(--r-pill);
  border: 1px solid var(--stl-hairline);
  color: var(--stl-text-faint);
  margin-left: 6px;
  vertical-align: 1px;
}
.stl-activity__outcome[data-outcome="denied"] {
  border-color: rgba(var(--stl-red-rgb), 0.4);
  background: rgba(var(--stl-red-rgb), 0.08);
  color: var(--stl-red-text);
}
.stl-activity__outcome[data-outcome="error"] {
  border-color: rgba(var(--stl-amber-rgb), 0.4);
  background: rgba(var(--stl-amber-rgb), 0.08);
  color: #fcd34d;
}
/* ok gets no pill at all — see renderActivityPanel. */

/* The footer normally holds one hint line; here it also holds the
 * Load more button, so it becomes a row with the button pushed right. */
.stl-activity .stl-admin__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
}
.stl-activity[data-busy="1"] .stl-admin__table-wrap { opacity: 0.5; }

/* ─── User detail modal ─── */
.stl-user-detail {
  display: grid;
  gap: var(--s-4);
}
.stl-user-detail__stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--s-2);
}
.stl-user-detail__stat {
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  padding: 12px var(--s-3);
}
.stl-user-detail__stat-lbl {
  font-family: var(--stl-font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-muted);
  margin: 0 0 4px;
}
.stl-user-detail__stat-num {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--stl-text);
  margin: 0;
  font-variant-numeric: tabular-nums;
}
.stl-user-detail__h {
  margin-bottom: 8px;
}
.stl-user-detail__topics {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}
.stl-user-detail__topic {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: var(--s-3);
  align-items: center;
  padding: 8px 12px;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-sm);
  font-size: 13px;
}
.stl-user-detail__topic-name {
  color: var(--stl-text);
  font-weight: 500;
}
.stl-user-detail__topic-acc {
  font-family: var(--stl-font-mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.stl-user-detail__topic[data-kind="strength"] .stl-user-detail__topic-acc {
  color: var(--stl-green-text);
}
.stl-user-detail__topic[data-kind="weakness"] .stl-user-detail__topic-acc {
  color: var(--stl-red-text);
}
.stl-user-detail__topic-n {
  color: var(--stl-text-faint);
  font-family: var(--stl-font-mono);
  font-size: 11px;
}
.stl-user-detail__empty {
  list-style: none;
  color: var(--stl-text-faint);
  font-size: 12.5px;
  padding: 8px 12px;
  background: var(--stl-surface);
  border: 1px dashed var(--stl-hairline);
  border-radius: var(--r-sm);
}
.stl-user-detail__tests {
  display: grid;
  gap: 6px;
  max-height: 240px;
  overflow-y: auto;
}
.stl-user-detail__test {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  gap: var(--s-3);
  align-items: center;
  padding: 10px 12px;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-sm);
  font-size: 13px;
}
.stl-user-detail__test-date {
  color: var(--stl-text-muted);
  font-size: 12px;
}
.stl-user-detail__test-target {
  color: var(--stl-text);
  font-family: var(--stl-font-mono);
  font-size: 12px;
}
.stl-user-detail__test-score {
  color: var(--stl-text);
  font-family: var(--stl-font-mono);
  font-variant-numeric: tabular-nums;
}
.stl-user-detail__test-time {
  color: var(--stl-text-faint);
  font-family: var(--stl-font-mono);
  font-size: 11px;
}

@media (max-width: 640px) {
  .stl-user-detail__stats { grid-template-columns: repeat(2, 1fr); }
}

/* --- main panel: filters + table + footer in one card --- */
.stl-admin__panel {
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  display: grid;
}

/* --- Imports tab — card grid + detail view ---
 * One card per registered import (from window.STL_IMPORTS). Cards are
 * a single column on tablet+ so the title and meta have room to
 * breathe; the chips strip across the middle gives an at-a-glance
 * breakdown of how questions in the import are distributed across
 * states. The detail view shares the .stl-admin__panel + .stl-admin__
 * table chrome with the Questions tab so per-row actions feel
 * consistent. */
.stl-imports__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
  gap: var(--s-4);
  padding: var(--s-4);
}
.stl-import-card {
  position: relative;
  background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  transition: border-color var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
.stl-import-card:hover {
  border-color: rgba(255,255,255,0.18);
}
/* Light mode: the hover color above is white, which is invisible on the
 * light card and reads as the border "disappearing". Use a darker hairline
 * so hover strengthens the border (matching the dark-mode intent). */
:root[data-theme="light"] .stl-import-card:hover {
  border-color: rgba(15, 23, 42, 0.22);
}
.stl-import-card.is-deleted {
  opacity: 0.65;
  background: rgba(255,255,255,0.02);
}
.stl-import-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
}
.stl-import-card__head-text {
  display: grid;
  gap: 4px;
  min-width: 0;
}
.stl-import-card__eyebrow {
  margin: 0;
  letter-spacing: 0.08em;
}
.stl-import-card__title {
  margin: 0;
  font-size: 19px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--stl-text);
  letter-spacing: -0.005em;
}
.stl-import-card__sub {
  margin: 0;
  font-size: 12px;
  color: var(--stl-text-subtle);
  letter-spacing: 0.01em;
}

/* State-breakdown chips — one per non-empty bucket. Colors mirror the
 * .stl-admin__badge--<state> palette so the chip and the table badge
 * read as the same thing. */
.stl-import-card__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.stl-import-card__chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  border-radius: var(--r-pill);
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.01em;
  border: 1px solid transparent;
}
.stl-import-card__chip--live          { background: rgba(var(--stl-green-rgb),0.10);  color: #4ade80; border-color: rgba(var(--stl-green-rgb),0.30); }
.stl-import-card__chip--needs-review  { background: rgba(var(--stl-yellow-rgb),0.10);  color: var(--stl-yellow); border-color: rgba(var(--stl-yellow-rgb),0.30); }
.stl-import-card__chip--unpublished   { background: rgba(148,163,184,0.10);color: #94a3b8; border-color: rgba(148,163,184,0.30); }
.stl-import-card__chip--archived      { background: rgba(var(--stl-red-rgb),0.10);  color: var(--stl-red); border-color: rgba(var(--stl-red-rgb),0.30); }

.stl-import-card__meta {
  margin: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--stl-hairline);
  font-size: 12px;
  color: var(--stl-text-subtle);
}
.stl-import-card__meta > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stl-import-card__meta dt {
  margin: 0;
  color: var(--stl-text-faint);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-size: 10px;
}
.stl-import-card__meta dd {
  margin: 0;
  color: var(--stl-text);
  word-break: break-word;
}
.stl-import-card__meta code {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  color: var(--stl-text-subtle);
}
.stl-import-card__actions {
  display: flex;
  gap: var(--s-2);
  margin-top: auto;
  flex-wrap: wrap;
}
.stl-import-card__actions .stl-btn { flex: 1 1 auto; min-width: 140px; }
.stl-import-card__actions .stl-btn--danger { flex: 0 1 auto; min-width: unset; }

/* Detail view header — back button, meta block, right-aligned action(s). */
.stl-admin__import-head {
  display: flex;
  align-items: flex-start;
  gap: var(--s-4);
  margin-bottom: var(--s-4);
}
.stl-admin__import-head .stl-admin__import-meta { flex: 1; }
.stl-admin__import-head .stl-admin__import-actions {
  display: flex;
  gap: var(--s-2);
  margin-top: 4px;
}

/* Danger pill — fallback if the design system hasn't shipped one yet.
 * The existing .stl-btn--ghost class supplies layout; we only need the
 * red treatment when the action is destructive. */
.stl-btn--danger {
  background: rgba(var(--stl-red-rgb), 0.08);
  border: 1px solid rgba(var(--stl-red-rgb), 0.5);
  color: var(--stl-red);
}
/* Pin the border AND the shadow, not just the fill. .stl-btn:hover sets
 * `border-color: var(--stl-accent-line)` plus a --stl-tint glow, and the
 * accent went GREEN with the signal theme — so a destructive button lit
 * up green-rimmed and green-glowing on hover, which is about as wrong as
 * a hover state gets (Josh 2026-08-11). Declaring only background/color
 * here left both of those inherited. Keeps a glow rather than killing
 * it: the convention is a tinted glow on hover, this one is just red. */
.stl-btn--danger:hover {
  background: rgba(var(--stl-red-rgb), 0.18);
  border-color: rgba(var(--stl-red-rgb), 0.75);
  color: var(--stl-red-text);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 8px 18px -8px rgba(var(--stl-red-rgb), 0.38);
}

.stl-admin__filter-summary {
  margin: 0;
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--stl-hairline);
  font-family: var(--stl-font-mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--stl-text-subtle);
}
.stl-admin__footer {
  padding: 12px var(--s-4);
  border-top: 1px solid var(--stl-hairline);
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  color: var(--stl-text-faint);
  letter-spacing: 0.04em;
  /* The tip sits left, the pager right. With the pager hidden the tip
     keeps its original position, so a small bank looks unchanged. */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
}

/* ----- questions table pager -----------------------------------------
   Lives in the footer rather than above the table so the filter row
   stays a single visual block and the controls sit next to the rows
   they page through. */
.stl-admin__pager {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
}
.stl-admin__pager[hidden] { display: none; }
.stl-admin__pager-size {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--stl-text-faint);
}
.stl-admin__pager-size select {
  font-family: inherit;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--stl-text);
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
  border-radius: 8px;
  padding: 4px 8px;
  cursor: pointer;
  transition: border-color 140ms ease, color 140ms ease;
}
.stl-admin__pager-size select:hover { border-color: var(--stl-hairline-hi); }
.stl-admin__pager-size select:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
/* Fixed min-width so "Page 9 of 88" and "Page 10 of 88" do not shift
   the Next button sideways as you page. */
.stl-admin__pager-pos {
  min-width: 11ch;
  text-align: center;
  color: var(--stl-text-subtle);
  font-variant-numeric: tabular-nums;
}
.stl-admin__pager-btn {
  font-size: 11px;
  padding: 5px 12px;
  min-width: 62px;
}
/* Disabled at the ends rather than hidden, so the footer never reflows
   as you reach the first or last page. */
.stl-admin__pager-btn:disabled {
  opacity: 0.38;
  cursor: default;
  pointer-events: none;
}
/* The table takes focus on page change (see goToAdminPage); it should
   not draw a focus ring, since the row content changing is the
   feedback. */
.stl-admin__table:focus { outline: none; }

.stl-admin__filters {
  display: grid;
  /* Default: Users panel uses just Search + Type + Reset. */
  grid-template-columns: 2.6fr 1fr auto;
  gap: var(--s-3);
  padding: var(--s-4);
  border-bottom: 1px solid var(--stl-hairline);
  align-items: end;
}
/* Questions panel — search + 6 filter dropdowns + Has-note toggle +
 * Reset, all on one row. The trailing `auto` slot keeps Reset locked
 * to the right edge. Source dropdown was removed 2026-05-25 (covered
 * by the AI/Human KPI tiles); Has-note toggle slots into the freed
 * column. Search is held at 1.4fr to leave room for the rest. */
.stl-admin__filters:has(#admin-filter-state) {
  grid-template-columns: 1.4fr repeat(7, 1fr) auto;
}

/* Pill-style binary toggle used by the Has-note filter. Visual: small
 * dot left of label, fills/colors when aria-pressed=true. Matches the
 * height of the filter <select> next to it so the row stays flat. */
.stl-admin__pill-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 38px;
  padding: 0 14px;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-pill);
  color: var(--stl-text-subtle);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition:
    background var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    color var(--dur-base) var(--ease-out);
}
.stl-admin__pill-toggle:hover {
  border-color: rgba(255, 255, 255, 0.3);
  color: var(--stl-text);
}
.stl-admin__pill-toggle__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
  transition: background var(--dur-base) var(--ease-out);
}
.stl-admin__pill-toggle[aria-pressed="true"] {
  background: rgba(var(--stl-green-rgb), 0.10);
  border-color: rgba(var(--stl-green-rgb), 0.55);
  color: #4ade80;
}
.stl-admin__pill-toggle[aria-pressed="true"] .stl-admin__pill-toggle__dot {
  background: #4ade80;
  box-shadow: 0 0 6px rgba(var(--stl-green-rgb), 0.55);
}
.stl-admin__field--search { grid-column: span 1; }
.stl-admin__field--actions { display: grid; gap: 4px; align-self: end; }
/* Reset button isn't full-width anymore — let it sit at its natural
 * width inside its column, anchored to the bottom row of the grid. */
.stl-admin__field--actions .stl-btn { width: auto; padding: 9px 14px; }
.stl-admin__field { display: grid; gap: 4px; }
.stl-admin__field-lbl {
  font-family: var(--stl-font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-admin__input,
.stl-admin__select {
  width: 100%;
  /* Pin both <input> and <select> to the SAME exact height so paired
   * fields in a 2-col row read as a clean horizontal line.
   *   - height (not min-height) so an input can't grow past it via
   *     iOS's 16px min-font-size or the user agent's intrinsic sizing
   *   - vertical padding swapped for line-height-based centering so
   *     the height stays predictable across input vs select rendering
   *   - box-sizing: border-box ensures border + padding fit inside */
  height: 40px;
  padding: 0 12px;
  font-family: inherit;
  font-size: 13.5px;
  line-height: 38px; /* height − 2 × border = vertical center */
  color: var(--stl-text);
  background: var(--stl-bg);
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-md);
  outline: none;
  transition: border-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out);
}
/* Strip the native select chrome and draw our own chevron so the
 * <select> is just a styled box — same height, same paddings, same
 * borders as the paired <input>. Fixes the long-running mismatch
 * where macOS/iOS rendered native selects shorter than inputs. */
.stl-admin__select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23a3a8b8' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 14px;
  padding-right: 36px;
}
/* Textarea is the one that legitimately needs to grow vertically;
 * undo the fixed height so resize works. */
.stl-admin__textarea {
  height: auto;
  min-height: 72px;
  padding: 9px 12px;
  line-height: 1.45;
}
.stl-admin__input:focus-visible,
.stl-admin__select:focus-visible {
  border-color: var(--stl-accent);
  box-shadow: 0 0 0 3px var(--stl-accent-soft);
}
.stl-admin__textarea {
  resize: vertical;
  min-height: 72px;
  font-family: inherit;
}

.stl-admin__table-wrap {
  overflow-x: auto;
  /* Inside the panel — no own border/background; let the panel chrome
   * be the frame. Bottom-rounded so the last row tucks under the panel
   * radius cleanly. */
  border-bottom-left-radius: var(--r-lg);
  border-bottom-right-radius: var(--r-lg);
  max-width: 100%;
}
.stl-admin__table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  /* Fixed layout so column widths come from the <colgroup> rules
   * below, NOT from whatever rows the current filter happens to
   * surface. Without this, switching test types or filters re-runs
   * the browser's auto-layout pass and reshuffles widths based on
   * the new contents — visually jarring and breaks any assumption
   * about how wide a stem preview can be. */
  table-layout: fixed;
}
/* Column widths — sum to 100%. Tuned for a 1200–1600px panel where
 * the question column gets most of the space, the structured columns
 * (state, source, diff, actions) take fixed pixel widths, and stats
 * gets enough room for the bar + percentage. */
.stl-admin__col--id      { width: auto; }       /* takes the leftover */
.stl-admin__col--state   { width: 100px; }
.stl-admin__col--source  { width: 110px; }
.stl-admin__col--subj    { width: 170px; }
.stl-admin__col--diff    { width: 72px; }
.stl-admin__col--date    { width: 76px; }
.stl-admin__col--stats   { width: 200px; }
.stl-admin__col--actions { width: 130px; }
.stl-admin__td-date {
  font-family: var(--stl-font-mono);
  font-size: 11.5px;
  color: var(--stl-text-subtle);
  white-space: nowrap;
}
.stl-admin__table thead {
  background: var(--stl-surface-2);
  position: sticky; top: 0; z-index: 1;
}
.stl-admin__table th {
  text-align: left;
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  font-weight: 500;
  padding: 11px var(--s-3);
  border-bottom: 1px solid var(--stl-hairline);
  white-space: nowrap;
  user-select: none;
}
/* Sortable headers — clickable, with a caret indicator that
 * appears on the active column. Inactive sortable headers show a
 * neutral caret on hover to telegraph "this is clickable." */
.stl-admin__th--sortable {
  cursor: pointer;
  /* Keep <th> as table-cell so the column layout still works.
   * The caret is rendered inline immediately after the label text
   * (see ::after below) with a small margin so it sits right next
   * to the label, regardless of how wide the column ends up. */
  transition: color var(--dur-base) var(--ease-spring);
}
.stl-admin__th--sortable:hover { color: var(--stl-text); }
.stl-admin__th--sortable::after {
  content: "↕";
  display: inline-block;
  margin-left: 6px;
  vertical-align: middle;
  opacity: 0;
  font-size: 10px;
  line-height: 1;
  color: var(--stl-text-faint);
  transition: opacity var(--dur-base) var(--ease-spring),
              color   var(--dur-base) var(--ease-spring);
}
.stl-admin__th--sortable:hover::after { opacity: 0.6; }
.stl-admin__th--sortable[data-sort-active]::after {
  opacity: 1;
  color: var(--stl-accent);
}
.stl-admin__th--sortable[data-sort-active="1"][data-sort-dir="asc"]::after  { content: "▲"; }
.stl-admin__th--sortable[data-sort-active="1"][data-sort-dir="desc"]::after { content: "▼"; }
.stl-admin__th--sortable[data-sort-active="1"] { color: var(--stl-text); }
.stl-admin__table td {
  padding: 12px var(--s-3);
  border-bottom: 1px solid var(--stl-hairline);
  vertical-align: middle;
}
/* The ID + stem cell stays top-aligned because its content is two
 * lines (mono ID + truncated stem); centering it would make short
 * stems float off the row's visual baseline. */
.stl-admin__table td.stl-admin__td-id { vertical-align: top; }
.stl-admin__row {
  position: relative;
  transition: background var(--dur-base) var(--ease-spring);
}
.stl-admin__row::after {
  /* Inset left accent for table rows — can't use ::before on a <tr>
   * cleanly, so we put it on the first cell via box-shadow on hover. */
  content: none;
}
/* Archived rows: recede until hover. Stem text gets a strikethrough
 * to underline (no pun) the soft-deleted feel, and the whole row drops
 * to ~55% opacity. Hover restores full opacity so action buttons stay
 * usable without squinting. The id stays at full strength so admins
 * can still copy it (⌘+click) at a glance. */
.stl-admin__row.is-archived {
  opacity: 0.6;
}
.stl-admin__row.is-archived .stl-admin__id-stem {
  text-decoration: line-through;
  text-decoration-color: var(--stl-hairline);
  text-decoration-thickness: 1px;
}
.stl-admin__row.is-archived:hover {
  opacity: 1;
}
.stl-admin__row:hover {
  background: var(--stl-surface-2);
  box-shadow: inset 3px 0 0 var(--stl-accent);
}
.stl-admin__th-id      { min-width: 220px; }
.stl-admin__th-stats   { min-width: 160px; }
.stl-admin__th-actions { width: 1%; white-space: nowrap; }
.stl-admin__td-id { min-width: 220px; }
.stl-admin__id {
  font-family: var(--stl-font-mono);
  font-size: 12px;
  font-weight: 500;
  color: var(--stl-text);
}
/* Small "has figure" indicator next to the row id — flags rows with
 * attached imagery (chart/svg/image/choiceCharts) so admins can spot
 * visual questions at a glance in a long table. */
.stl-admin__figure-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 0 4px;
  border-radius: 3px;
  background: rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.14);
  color: var(--stl-accent);
  font-size: 11px;
  vertical-align: middle;
  cursor: help;
}
/* Sibling of .stl-admin__figure-badge — same chip shape but yellow-
 * tinted so the two indicators are visually distinct sitting next to
 * each other. Marks rows that have an admin note (text and/or any
 * attachments) so they're scannable in a long table. */
.stl-admin__note-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 4px;
  padding: 1px 4px;
  border-radius: 3px;
  background: rgba(var(--stl-yellow-rgb), 0.16);
  color: var(--stl-yellow-text);
  vertical-align: middle;
  cursor: help;
  line-height: 1;
}
.stl-admin__note-badge svg {
  display: block;
}
.stl-admin__id-stem {
  font-size: 12.5px;
  color: var(--stl-text-muted);
  margin-top: 3px;
  line-height: 1.4;
  /* Stem previews can run long. Truncate to two lines so the row
   * height stays consistent regardless of stem length. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
.stl-admin__uploader {
  font-size: 11px;
  color: var(--stl-text-subtle);
  margin-top: 2px;
}

/* Per-state badges. Live = green, Unpublished = subtle, Needs review = amber/red. */
.stl-admin__badge {
  display: inline-flex; align-items: center;
  padding: 3px 8px;
  border-radius: var(--r-pill);
  font-family: var(--stl-font-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 1px solid var(--stl-hairline);
  font-weight: 500;
  white-space: nowrap;          /* "NEEDS REVIEW" stays on one line */
}
.stl-admin__badge--live {
  color: var(--stl-green-text);
  border-color: rgba(var(--stl-green-rgb), 0.45);
  background: rgba(var(--stl-green-rgb), 0.08);
}
.stl-admin__badge--unpublished {
  color: var(--stl-text-subtle);
  border-color: var(--stl-hairline);
}
.stl-admin__badge--needs-review {
  color: var(--stl-yellow-text);
  border-color: rgba(var(--stl-yellow-rgb), 0.45);
  background: rgba(var(--stl-yellow-rgb), 0.08);
}
/* Archived = soft-deleted. Visually receded — no chroma, dashed
 * border so it reads as a "frozen" state distinct from Unpublished
 * (which is just paused). Dimmer than the body text by design so
 * archived rows that DO surface (via the State filter) look quiet. */
.stl-admin__badge--archived {
  color: var(--stl-text-subtle);
  border: 1px dashed var(--stl-hairline);
  background: transparent;
  opacity: 0.85;
}
/* Deleted — visually distinct from archived. Red-tinted, strikethrough
 * label to signal that the question is gone from the bank (not just
 * paused). Restoration requires editing the override in localStorage
 * until we ship a UI affordance, so make the look match the severity. */
.stl-admin__badge--deleted {
  color: var(--stl-red);
  border: 1px solid rgba(var(--stl-red-rgb), 0.55);
  background: rgba(var(--stl-red-rgb), 0.08);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}
/* Light-mode badge contrast. The dark-theme pastels (#bbf7d0 green,
 * #fde68a amber, #f87171 red) are near-invisible on a white surface —
 * the "LIVE" pill in particular washed out completely. Swap to the
 * darker saturated status-text tokens (same ones the pills/labels use),
 * which clear WCAG AA on white (#15803d ≈ 4.8:1, #a16207 ≈ 4.7:1,
 * #b91c1c ≈ 5.9:1). Borders/backgrounds re-tinted to match. The
 * unpublished + archived badges already use adaptive tokens, so they're
 * left alone. */
:root[data-theme="light"] .stl-admin__badge--live {
  color: var(--stl-green-text);
  border-color: rgba(var(--stl-green-rgb), 0.45);
  background: rgba(var(--stl-green-rgb), 0.10);
}
:root[data-theme="light"] .stl-admin__badge--needs-review {
  color: var(--stl-yellow-text);
  border-color: rgba(202, 138, 4, 0.50);
  background: rgba(202, 138, 4, 0.10);
}
:root[data-theme="light"] .stl-admin__badge--deleted {
  color: var(--stl-red-text);
  border-color: rgba(var(--stl-red-rgb), 0.55);
  background: rgba(var(--stl-red-rgb), 0.10);
}

/* Difficulty cell — show the SAT score number, tinted by its
 * tier band so the admin can spot distribution at a glance. */
.stl-admin__diff {
  display: inline-block;
  font-family: var(--stl-font-mono);
  font-size: 12px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  padding: 2px 7px;
  border-radius: var(--r-sm);
  border: 1px solid var(--stl-hairline);
  color: var(--stl-text-muted);
}
.stl-admin__diff[data-tier="easy"]   { color: var(--stl-green-text);  border-color: rgba(var(--stl-green-rgb), 0.35); }
.stl-admin__diff[data-tier="medium"] { color: var(--stl-yellow-text); border-color: rgba(var(--stl-yellow-rgb), 0.35); }
.stl-admin__diff[data-tier="hard"]   { color: var(--stl-red-text);               border-color: rgba(var(--stl-red-rgb), 0.4); }
.stl-admin__diff[data-tier="insane"] { color: var(--stl-blue-text);               border-color: rgba(var(--stl-blue-rgb), 0.4); }

/* Source pill — small, right-aligned next to uploader email. */
.stl-admin__source {
  display: inline-flex;
  padding: 2px 7px;
  border-radius: var(--r-pill);
  font-family: var(--stl-font-mono);
  font-size: 9.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 1px solid var(--stl-hairline);
  font-weight: 500;
}
.stl-admin__source--human-curated { color: var(--stl-text-muted); }
.stl-admin__source--ai-generated {
  color: var(--stl-accent);
  border-color: var(--stl-accent-line);
  background: var(--stl-accent-soft);
}

/* Stat bar — three weighted segments per signal color, with a
 * trailing percentage + attempt count. Empty when there are no
 * recorded attempts for the question yet. */
.stl-admin__stats {
  display: inline-grid;
  grid-template-columns: 100px auto;
  align-items: center;
  gap: var(--s-2);
}
.stl-admin__statbar {
  display: inline-flex;
  height: 6px;
  width: 100px;
  border-radius: var(--r-pill);
  overflow: hidden;
  background: var(--stl-surface-3);
}
.stl-admin__statbar-seg { height: 100%; }
.stl-admin__statbar-seg--g { background: var(--stl-green); }
.stl-admin__statbar-seg--y { background: var(--stl-yellow); }
.stl-admin__statbar-seg--r { background: var(--stl-red); }
.stl-admin__stats-text {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  color: var(--stl-text-subtle);
  white-space: nowrap;
}
.stl-admin__stats-empty {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  color: var(--stl-text-faint);
}

/* Action cluster — keep this as a regular table-cell (DON'T make it a
 * flex container) so it inherits `vertical-align: top` from the global
 * td rule and the buttons align with the row's other cells at the top.
 * Buttons themselves are inline-flex, so they sit side-by-side
 * horizontally; the adjacent-sibling margin handles spacing. */
.stl-admin__td-actions {
  white-space: nowrap;
  text-align: right;
  /* Top-align so the icon buttons sit on the first-line baseline of
   * neighboring 2-line cells (Question id + stem, Source + uploader,
   * Subject + topic). Default vertical-align: middle pushed them
   * visually below those cells' content. */
  vertical-align: top;
}
.stl-admin__td-actions > .stl-history__action + .stl-history__action {
  margin-left: 4px;
}
/* Spacing between icon buttons (the new admin row action cluster).
 * Uses the adjacent-sibling rule so the td stays a true table cell
 * (display: flex on td breaks row vertical alignment). */
.stl-admin__td-actions > .stl-admin__icon-btn + .stl-admin__icon-btn {
  margin-left: 4px;
}

/* ---- Subject / Topic 2-line cell ----------------------------------
 * Subject is the major category (Math, Reading & Writing, etc.) shown
 * as the primary line. Topic is the granular tag shown beneath in a
 * smaller, dimmer style — same hierarchy as the other 2-line cells in
 * the table (Question id + stem preview, Source badge + uploader). */
.stl-admin__td-subj {
  vertical-align: top;
  min-width: 140px;
}
.stl-admin__subj {
  font-size: 13px;
  font-weight: 500;
  color: var(--stl-text);
  line-height: 1.3;
}
.stl-admin__topic {
  font-size: 11.5px;
  color: var(--stl-text-subtle);
  font-family: var(--stl-font-mono);
  letter-spacing: 0.02em;
  margin-top: 2px;
  line-height: 1.3;
  /* Topic IDs (kebab-case) can run long — truncate with ellipsis so
   * the column stays its declared width regardless of content. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- Action icon buttons ------------------------------------------
 * Compact icon-only buttons (Lucide-style strokes). Each one carries
 * a CSS-tooltip via [data-tip] (the pseudo-element below) and a
 * native title= attribute as a fallback. Sized so a row of 3 buttons
 * stays inside the actions column without forcing the table to widen.
 * Right-alignment + button spacing come from .stl-admin__td-actions
 * above (which keeps the td a true table-cell for proper row
 * vertical-align behavior). */
.stl-admin__icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border-radius: var(--r-sm);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  color: var(--stl-text-subtle);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.stl-admin__icon-btn:hover {
  background: var(--stl-accent-soft);
  border-color: var(--stl-accent-line);
  color: var(--stl-text);
  transform: translateY(-1px);
}
.stl-admin__icon-btn:active { transform: translateY(0); }
.stl-admin__icon-btn:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
.stl-admin__icon-btn--danger:hover {
  background: rgba(var(--stl-red-rgb), 0.10);
  border-color: rgba(var(--stl-red-rgb), 0.40);
  color: rgba(var(--stl-red-rgb), 0.92);
}

/* Body-level tooltip — rendered as a single shared #stl-floating-tip
 * element appended to <body> by initAdmin. Positioned via JS using
 * getBoundingClientRect of the hovered icon-btn, so it escapes any
 * parent overflow:hidden / overflow:auto clip context (the table-wrap
 * has overflow-x:auto, which would otherwise crop the tooltip). */
.stl-floating-tip {
  position: fixed;
  top: 0;
  left: 0;
  padding: 5px 9px;
  border-radius: var(--r-sm);
  background: var(--stl-surface-3);
  color: var(--stl-text);
  font-size: 11px;
  font-family: var(--stl-font-mono);
  letter-spacing: 0.02em;
  white-space: nowrap;
  border: 1px solid var(--stl-hairline-hi);
  pointer-events: none;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 120ms ease, transform 120ms ease;
  z-index: 9999;
}
.stl-floating-tip[data-show="1"] {
  opacity: 1;
  transform: translateY(0);
}

.stl-admin__empty {
  text-align: center;
  padding: var(--s-6);
  color: var(--stl-text-muted);
  margin: 0;
}
.stl-admin__hint {
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  color: var(--stl-text-faint);
}

/* ============== edit modal ==============
 * Centered drawer with a darkening backdrop. Form is a stack of
 * labeled inputs; choices are a vertical list of letter+input rows. */
.stl-admin-modal {
  position: fixed; inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: var(--s-4);
}
.stl-admin-modal[hidden] { display: none; }
/* Backdrop and panel mount transparent and animate in when the modal
 * gains the `.is-open` class. JS adds the class on the next frame
 * after un-hiding (so transitions fire) and removes it before hiding
 * again, with `hidden = true` deferred until the transition finishes
 * (see openAdminModal/closeAdminModal helpers in app.js). */
.stl-admin-modal__backdrop {
  position: absolute; inset: 0;
  background: rgba(5, 5, 9, 0);
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
  transition:
    background var(--dur-base) var(--ease-out),
    backdrop-filter var(--dur-base) var(--ease-out),
    -webkit-backdrop-filter var(--dur-base) var(--ease-out);
}
.stl-admin-modal.is-open .stl-admin-modal__backdrop {
  background: rgba(5, 5, 9, 0.72);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.stl-admin-modal__panel {
  position: relative;
  width: 100%;
  max-width: 640px;
  /* Constrain to (viewport - outer container padding). The outer
   * .stl-admin-modal has padding: var(--s-4) on every side, so the
   * panel must subtract 2 * --s-4 from 100dvh to fit inside the
   * gutter. The previous value (100dvh - --s-6=24px) over-shot by
   * 8px, which let tall forms (e.g. "New question — manual") push
   * past the viewport bottom and cut off the Save button.
   *
   * Use 100dvh (dynamic viewport) so mobile browser chrome appearing/
   * disappearing doesn't push the modal off-screen. */
  max-height: calc(100dvh - 2 * var(--s-4));
  overflow-y: auto;
  /* Defensive: belt-and-suspenders flag any future child that tries
   * to set its own height — the panel is the single overflow boundary
   * and inner sections must rely on it for scrolling. */
  overscroll-behavior: contain;
  background:
    linear-gradient(165deg, rgba(148, 163, 184, 0.06) 0%, rgba(255, 255, 255, 0.02) 50%, transparent 80%),
    var(--stl-bg);
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-lg);
  box-shadow: 0 24px 60px -12px rgba(0, 0, 0, 0.6);
  display: grid;
  gap: var(--s-4);
  padding: var(--s-5);
  opacity: 0;
  transform: translateY(8px) scale(0.98);
  transition:
    opacity var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}
.stl-admin-modal.is-open .stl-admin-modal__panel {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .stl-admin-modal__backdrop,
  .stl-admin-modal__panel { transition: none; }
  .stl-admin-modal__panel { transform: none; }
}

/* --- Generic confirm dialog ---
 * The app's replacement for window.confirm(). Same shell as the edit
 * modal (backdrop, transitions, reduced-motion) at a much smaller size:
 * this holds one question and two buttons, and a 640px panel around
 * that much content reads as an empty box.
 *
 * z-index is raised above the base modal because a confirm can be
 * raised ON TOP of an open modal (deleting from the user-detail modal,
 * say) and must not render underneath the thing that spawned it. */
.stl-confirm { z-index: 300; }
.stl-confirm__panel {
  max-width: 420px;
  gap: var(--s-4);
}
.stl-confirm__msg {
  margin: 6px 0 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--stl-text-muted);
}
.stl-confirm__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
}
/* Stack on narrow screens, confirm on top. Side-by-side buttons at this
 * width shrink until both labels wrap, and a wrapped "Leave test" next
 * to a wrapped "Keep going" is genuinely hard to tell apart at a
 * glance — which is the one moment where misreading it costs an
 * in-progress attempt. */
@media (max-width: 420px) {
  .stl-confirm__actions { flex-direction: column-reverse; }
  .stl-confirm__actions .stl-btn { width: 100%; }
}

/* --- Review queue modal — fullscreen single-question triage ---
 * Wider + taller than the standard edit modal so the question, choices,
 * figure, AND explanation all fit on one screen without scrolling for
 * the common case. The base .stl-admin-modal__panel sets overflow-y:
 * auto + padding + gap so its content scrolls as one block; for review
 * we want a fixed head/foot with ONLY the body scrolling, so we
 * override those properties below. Without that override the eyebrow
 * + decision buttons scroll off-screen on shorter viewports.
 *
 * Specificity bump (.stl-admin-modal .stl-review-modal__panel) so we
 * win over the base class without an !important. */
.stl-admin-modal .stl-review-modal__panel {
  max-width: min(960px, calc(100vw - 32px));
  width: 100%;
  max-height: calc(100dvh - 2 * var(--s-4));
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 0;
  padding: 0;
  /* Disable the panel-level scroll — the inner body owns scrolling.
   * Same for the outer panel's contain so the body's scroll doesn't
   * bleed into the page. */
  overflow: hidden;
  overscroll-behavior: contain;
}
.stl-review-modal__head {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--stl-hairline);
}
.stl-review-modal__head-left {
  display: grid;
  gap: 2px;
}
.stl-review-modal__head-left .stl-admin-modal__title {
  margin: 0;
  font-size: 14px;
  letter-spacing: 0.02em;
}
.stl-review-modal__head-meta {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--stl-font-mono);
  font-size: 11px;
  color: var(--stl-text-subtle);
}
.stl-review-modal__meta-id { letter-spacing: 0.02em; }
.stl-review-modal .stl-admin-modal__close { position: static; }

/* Participant-view link in the head — sits between meta and close. */
.stl-review-modal__participant {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-pill);
  color: var(--stl-text-subtle);
  font-size: 12px;
  letter-spacing: 0.01em;
  text-decoration: none;
  transition:
    color var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    background var(--dur-base) var(--ease-out);
}
.stl-review-modal__participant:hover {
  color: var(--stl-text);
  border-color: rgba(255,255,255,0.3);
  background: rgba(255,255,255,0.04);
}
.stl-review-modal__participant svg { opacity: 0.7; }
.stl-review-modal__participant:hover svg { opacity: 1; }

/* Preview banner — mounts at top of body when ?preview-question= is set. */
.stl-preview-banner {
  position: sticky;
  top: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 10px var(--s-4);
  background: linear-gradient(180deg, rgba(var(--stl-yellow-rgb),0.18), rgba(var(--stl-yellow-rgb),0.10));
  border-bottom: 1px solid rgba(var(--stl-yellow-rgb),0.45);
  color: var(--stl-text);
  font-size: 13px;
}
.stl-preview-banner__badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 4px;
  background: rgba(var(--stl-yellow-rgb),0.25);
  border: 1px solid rgba(var(--stl-yellow-rgb),0.55);
  color: var(--stl-yellow);
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  font-weight: 600;
}
.stl-preview-banner__text {
  flex: 1;
  color: var(--stl-text-subtle);
}
.stl-preview-banner__text code {
  font-family: var(--stl-font-mono);
  font-size: 12px;
  color: var(--stl-text);
}
.stl-preview-banner__close {
  background: transparent;
  border: 1px solid var(--stl-hairline);
  border-radius: 4px;
  padding: 4px 10px;
  color: var(--stl-text-subtle);
  font-size: 12px;
  cursor: pointer;
}
.stl-preview-banner__close:hover {
  color: var(--stl-text);
  border-color: rgba(255,255,255,0.3);
}

/* Body fills remaining height; scrolls if content exceeds. The
 * `min-height: 0` is what lets the 1fr grid row actually constrain
 * below its intrinsic content size — without it, grid children
 * default to min-content sizing and the row grows to fit, defeating
 * the panel's max-height. */
.stl-review-modal__body[hidden] { display: none; }
.stl-review-modal__body {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-5) var(--s-5) var(--s-4);
}

/* Card content — looser leading, larger stem so the reviewer can read
 * comfortably at arm's length. */
.stl-review-card { display: grid; gap: var(--s-4); }
.stl-review-card__stem {
  font-size: 17px;
  line-height: 1.55;
  color: var(--stl-text);
  white-space: pre-wrap;
}
.stl-review-card__figure {
  margin: 0;
  padding: var(--s-3);
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  display: flex;
  justify-content: center;
}
.stl-review-card__figure svg {
  max-width: 100%;
  height: auto;
}
.stl-review-card__table-wrap {
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  padding: var(--s-3);
  overflow-x: auto;
}

/* Choices — letter + text in a card; the correct answer is highlighted
 * with the green signal color so the reviewer can verify at a glance. */
.stl-review-card__choices {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-2);
}
.stl-review-card__choice {
  display: grid;
  grid-template-columns: 24px 1fr auto;
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-3) var(--s-4);
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  font-size: 15px;
  line-height: 1.4;
}
.stl-review-card__choice.is-correct {
  border-color: rgba(var(--stl-green-rgb),0.55);
  background: rgba(var(--stl-green-rgb),0.08);
}
.stl-review-card__choice-letter {
  font-family: var(--stl-font-mono);
  font-size: 12px;
  color: var(--stl-text-subtle);
}
.stl-review-card__choice.is-correct .stl-review-card__choice-letter { color: #4ade80; }
.stl-review-card__choice-mark {
  font-weight: 700;
  color: #4ade80;
}

/* SPR — show the typed answer in a callout. */
.stl-review-card__spr {
  padding: var(--s-4);
  background: rgba(var(--stl-green-rgb),0.08);
  border: 1px solid rgba(var(--stl-green-rgb),0.55);
  border-radius: var(--r-md);
  text-align: center;
}
.stl-review-card__spr-label {
  margin: 0 0 6px;
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-review-card__spr-value {
  margin: 0;
  font-size: 24px;
  font-weight: 600;
  color: #4ade80;
}

.stl-review-card__explanation {
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  padding: var(--s-3) var(--s-4);
}
.stl-review-card__explanation > summary {
  cursor: pointer;
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  user-select: none;
}
.stl-review-card__explanation-body {
  margin-top: var(--s-3);
  font-size: 14px;
  line-height: 1.55;
  white-space: pre-wrap;
  color: var(--stl-text);
}

/* Per-question Note — inline textarea. Visible in both review and
 * (future) standalone view contexts. Auto-saves on blur and on every
 * decision, keyed by the textarea's data-qid attribute. */
.stl-review-card__note {
  display: grid;
  gap: 6px;
  padding: var(--s-3);
  background: rgba(255,255,255,0.03);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
}
.stl-review-card__note .stl-eyebrow {
  margin: 0;
  color: var(--stl-text-faint);   /* colour only — format is the primitive's */
}
.stl-review-card__note-input {
  width: 100%;
  min-height: 60px;
  resize: vertical;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-sm);
  color: var(--stl-text);
  font: inherit;
  font-size: 14px;
  line-height: 1.5;
  padding: 10px 12px;
}
.stl-review-card__note-input:focus {
  outline: none;
  border-color: rgba(255,255,255,0.3);
}
.stl-review-card__note-hint {
  margin: 0;
  font-size: 11px;
  color: var(--stl-text-faint);
  font-family: var(--stl-font-mono);
  letter-spacing: 0.02em;
}

/* Image attachments — thumbnail strip + drag/drop affordance. The
 * note container gets a subtle outline when a file is dragged over
 * it; thumbnails sit inline so multiple attachments wrap naturally. */
.stl-review-card__note.is-drag-over {
  border-color: rgba(var(--stl-green-rgb), 0.55);
  background: rgba(var(--stl-green-rgb), 0.06);
}
.stl-review-card__attachments {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}
.stl-review-card__attachments:empty { display: none; }
.stl-review-card__attach {
  position: relative;
  margin: 0;
  width: 96px;
  display: grid;
  gap: 4px;
}
.stl-review-card__attach a {
  display: block;
  border-radius: var(--r-sm);
  overflow: hidden;
  border: 1px solid var(--stl-hairline);
  background: rgba(0,0,0,0.3);
}
.stl-review-card__attach img {
  display: block;
  width: 100%;
  height: 72px;
  object-fit: cover;
}
.stl-review-card__attach figcaption {
  font-family: var(--stl-font-mono);
  font-size: 10px;
  color: var(--stl-text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.stl-review-card__attach-remove {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1px solid rgba(0,0,0,0.5);
  background: rgba(0,0,0,0.7);
  color: #fff;
  cursor: pointer;
  font-size: 12px;
  line-height: 18px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.stl-review-card__attach-remove:hover {
  background: rgba(var(--stl-red-rgb), 0.85);
  border-color: rgba(var(--stl-red-rgb), 0.95);
}
.stl-review-card__attach-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
}
.stl-review-card__attach-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  font-size: 12px;
  cursor: pointer;
}
.stl-review-card__attach-hint {
  margin: 0;
  font-size: 11px;
  color: var(--stl-text-faint);
}

.stl-review-card__meta {
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 8px var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--stl-hairline);
  font-size: 12px;
}
.stl-review-card__meta > div { display: flex; gap: 8px; }
.stl-review-card__meta dt {
  margin: 0;
  color: var(--stl-text-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 10.5px;
  min-width: 64px;
}
.stl-review-card__meta dd { margin: 0; color: var(--stl-text); }

/* Sticky footer — decisions are always one click away. */
.stl-review-modal__foot[hidden] { display: none; }
.stl-review-modal__foot {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--s-4);
  align-items: center;
  padding: var(--s-3) var(--s-4);
  border-top: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
}
.stl-review-modal__nav {
  display: flex;
  gap: var(--s-2);
  align-items: center;
}
.stl-review-modal__shortcuts {
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  color: var(--stl-text-faint);
  letter-spacing: 0.02em;
  margin: 0 var(--s-3);
}
.stl-review-modal__shortcuts kbd {
  font-family: inherit;
  background: rgba(255,255,255,0.08);
  border: 1px solid var(--stl-hairline);
  border-radius: 4px;
  padding: 1px 6px;
  margin: 0 2px;
  color: var(--stl-text-subtle);
}
.stl-review-modal__decisions {
  display: flex;
  gap: var(--s-2);
}

/* Done state — replaces the body when the queue is exhausted. The
 * explicit [hidden] override is required because the unprefixed
 * `.stl-review-modal__done { display: grid }` below would otherwise win
 * the specificity tie against the UA `[hidden] { display: none }` and
 * the done block would render alongside the live card. Same hazard for
 * any class-styled element using `hidden` as a visibility toggle. */
.stl-review-modal__done[hidden] { display: none; }
.stl-review-modal__done {
  grid-column: 1 / -1;
  padding: var(--s-6) var(--s-5);
  text-align: center;
  display: grid;
  gap: var(--s-3);
  justify-items: center;
}
.stl-review-modal__done h3 {
  margin: 0;
  font-size: 24px;
  font-weight: 600;
  color: var(--stl-text);
}

/* Trigger-button count chip. */
.stl-btn__count {
  display: inline-block;
  min-width: 18px;
  padding: 0 6px;
  margin-left: 6px;
  background: rgba(255,255,255,0.12);
  border-radius: var(--r-pill);
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  line-height: 16px;
  vertical-align: 1px;
  text-align: center;
  color: var(--stl-text);
}
.stl-admin-modal__head {
  display: grid;
  gap: 4px;
  padding-right: 36px;
  position: relative;
}
.stl-admin-modal__title {
  font-family: var(--stl-font-mono);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
  color: var(--stl-text);
}
.stl-admin-modal__close {
  position: absolute;
  top: 0; right: 0;
  appearance: none; -webkit-appearance: none;
  background: transparent;
  border: 1px solid var(--stl-hairline);
  color: var(--stl-text-muted);
  font: inherit;
  font-size: 18px;
  line-height: 1;
  width: 32px; height: 32px;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out);
}
.stl-admin-modal__close:hover {
  border-color: var(--stl-hairline-hi);
  color: var(--stl-text);
}

.stl-admin-modal__form { display: grid; gap: var(--s-3); }
.stl-admin__row-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-3);
  /* Top-align fields so each label sits flush with its own input.
   * Without this, a hint in one column stretches the row and the
   * other column's input drifts down to fill the empty space —
   * breaking the label↔input visual pairing. */
  align-items: start;
}
.stl-admin__field-group {
  border: 0; padding: 0; margin: 0;
  display: grid; gap: 10px;
}
/* The A/B/C/D choice rows live in their own container inside the
 * field-group. Give them explicit vertical breathing room — without
 * a gap they stack tightly against each other and feel cramped. */
#add-choices-list,
#edit-choices-list {
  display: grid;
  gap: 10px;
}
.stl-admin__choice-row {
  display: grid;
  grid-template-columns: 32px 1fr;
  gap: var(--s-2);
  align-items: center;
}
.stl-admin__choice-letter {
  display: grid; place-items: center;
  width: 28px; height: 28px;
  border-radius: var(--r-sm);
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline-hi);
  font-family: var(--stl-font-mono);
  font-size: 11px;
  color: var(--stl-text-muted);
  font-weight: 500;
}
/* Figure editor — preview pane inside the Edit modal. Renders the
 * current figure config live as the admin types so they can iterate
 * without saving. */
.stl-admin__figure-preview {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-3);
  margin-top: var(--s-2);
  border: 1px dashed var(--stl-hairline);
  border-radius: var(--r-md);
  background: var(--stl-surface);
}
.stl-admin__figure-preview .stl-quiz__figure {
  margin: 0;
  /* Fixed height — Chart.js with maintainAspectRatio:false fills its
   * container, and an unbounded parent creates a render loop where
   * the canvas keeps growing on each ResizeObserver tick. 200px is
   * enough to inspect the chart shape without dominating the modal. */
  height: 200px;
}
.stl-admin__figure-preview .stl-quiz__figure[data-kind="image"],
.stl-admin__figure-preview .stl-quiz__figure[data-kind="svg"] {
  /* Image / SVG aren't canvases — let them keep their natural
   * aspect ratio, just cap so a tall figure doesn't dominate. */
  height: auto;
  max-height: 240px;
}
/* Choice-charts mini-preview grid — 2x2 layout matching the live
 * quiz's choice card grid, scaled down for the modal. Each card has
 * a FIXED height so the inner Chart.js canvas (responsive=true,
 * maintainAspectRatio=false) sizes deterministically instead of
 * growing every render tick. */
.stl-admin__choice-preview {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-2);
}
.stl-admin__choice-preview-card {
  display: grid;
  grid-template-rows: auto 1fr;
  align-items: stretch;
  padding: 6px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-sm);
  background: var(--stl-surface-2);
  height: 150px;
  overflow: hidden;
}
.stl-admin__choice-preview-card canvas {
  /* Pin canvas to the available 1fr row so Chart.js doesn't compute
   * height from its rendered text and grow on each measurement pass. */
  width: 100% !important;
  height: 100% !important;
}
.stl-admin__choice-preview-card .stl-admin__choice-letter {
  justify-self: start;
  margin-bottom: 4px;
}
/* Inline error variant for the JSON-config fields in the figure
 * editor — surfaces parse errors without opening a toast. */
.stl-hint--err {
  color: var(--stl-red);
}

.stl-admin-modal__meta {
  border-top: 1px solid var(--stl-hairline);
  padding-top: var(--s-3);
}
.stl-admin__meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--s-3);
  margin: 0;
}
.stl-admin__meta dt {
  font-family: var(--stl-font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-admin__meta dd {
  margin: 2px 0 0;
  font-size: 12.5px;
  color: var(--stl-text);
}
.stl-admin-modal__actions {
  display: flex;
  gap: var(--s-2);
  justify-content: flex-end;
  flex-wrap: wrap;
  padding-top: var(--s-2);
  border-top: 1px solid var(--stl-hairline);
}

/* Tablet — collapse KPI strip to 3 columns, filters to 2. */
@media (max-width: 1024px) {
  .stl-admin__kpis { grid-template-columns: repeat(3, 1fr); }
  .stl-admin__filters {
    grid-template-columns: 1fr 1fr;
  }
  .stl-admin__field--search { grid-column: 1 / -1; }
  .stl-admin__field--actions { grid-column: 1 / -1; }
  .stl-admin__field--actions .stl-btn { width: auto; }
}

/* Phone — 2-col KPIs, single-col filters, modal fields stack */
@media (max-width: 640px) {
  .stl-admin__kpis { grid-template-columns: repeat(2, 1fr); }
  .stl-admin__filters { grid-template-columns: 1fr; padding: var(--s-3); }
  .stl-admin__row-2 { grid-template-columns: 1fr; }
}

/* ============== ADD QUESTIONS MODAL ============== *
 * Card-picker → drill-in (Option B). Reuses the .stl-admin-modal
 * positioning chrome (centered overlay with blurred backdrop). The
 * panel widens to fit 3 picker cards; the body switches between
 * picker / manual / image / csv via [data-view] attributes — JS
 * toggles the [hidden] attr on each view. Drag-and-drop on the
 * panel auto-routes to the matching flow based on file extension.
 */
.stl-add-modal__panel { max-width: 760px; }
.stl-add-modal__back {
  position: absolute;
  top: 0; left: 0;
  appearance: none; -webkit-appearance: none;
  background: transparent;
  border: 1px solid transparent;
  color: var(--stl-text-muted);
  font: inherit;
  font-size: 18px;
  width: 32px; height: 32px;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: color   var(--dur-base) var(--ease-spring),
              border-color var(--dur-base) var(--ease-spring),
              background var(--dur-base) var(--ease-spring);
}
.stl-add-modal__back:hover {
  color: var(--stl-text);
  border-color: var(--stl-accent-line);
  background: var(--stl-accent-soft);
}
.stl-add-modal__back[hidden] { display: none; }
/* Indent head text right when back arrow is visible. */
.stl-add-modal:not(.is-picker) .stl-admin-modal__head { padding-left: 44px; }

.stl-add-modal__view[hidden] { display: none; }
.stl-add-modal__view {
  display: grid;
  gap: var(--s-4);
  animation: stl-add-view-in 280ms var(--ease-spring) both;
}
@keyframes stl-add-view-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .stl-add-modal__view { animation: none; }
}
.stl-add-modal__hint {
  margin: 0;
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--stl-text-faint);
  text-align: center;
}

/* Picker — three cards in a 3-col grid. Each card behaves like a
 * .stl-stat tile: lavender accent on hover, focus ring, etc.
 * .is-drag-over class adds a glow when a file is being dragged
 * over the card so the user sees which path is about to fire. */
.stl-add-picker {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s-3);
}
@media (min-width: 900px) {
  .stl-add-picker { grid-template-columns: repeat(4, 1fr); }
}
.stl-add-picker__card {
  appearance: none; -webkit-appearance: none;
  font: inherit; cursor: pointer;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  padding: var(--s-5) var(--s-4);
  text-align: left;
  display: grid;
  gap: 8px;
  align-content: start;
  position: relative;
  transition: background  var(--dur-base) var(--ease-spring),
              border-color var(--dur-base) var(--ease-spring),
              transform   var(--dur-fast) var(--ease-spring),
              box-shadow  var(--dur-base) var(--ease-spring);
}
.stl-add-picker__card:hover {
  background: var(--stl-surface-2);
  border-color: var(--stl-accent-line);
  transform: translateY(-1px);
  box-shadow: 0 12px 28px -10px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.4);
}
.stl-add-picker__card:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
.stl-add-picker__card.is-drag-over {
  border-color: var(--stl-accent);
  background: var(--stl-accent-soft);
  box-shadow: 0 0 0 3px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.18);
}
.stl-add-picker__icon {
  display: grid;
  place-items: center;
  width: 36px; height: 36px;
  border-radius: var(--r-sm);
  background: var(--stl-accent-soft);
  border: 1px solid var(--stl-accent-line);
  color: var(--stl-accent);
}
.stl-add-picker__h {
  font-size: 15px;
  font-weight: 600;
  color: var(--stl-text);
  letter-spacing: -0.01em;
}
.stl-add-picker__sub {
  font-size: 13px;
  line-height: 1.45;
  color: var(--stl-text-muted);
}
.stl-add-picker__sub a { color: var(--stl-accent); text-decoration: none; }
.stl-add-picker__sub a:hover { text-decoration: underline; }

/* Drop zones — used inside the image and CSV drill-in views. */
.stl-dropzone {
  position: relative;
  border: 1px dashed var(--stl-hairline-hi);
  border-radius: var(--r-md);
  padding: var(--s-7) var(--s-4);
  text-align: center;
  background: var(--stl-surface);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-spring),
              background   var(--dur-base) var(--ease-spring);
}
.stl-dropzone:hover {
  border-color: var(--stl-accent-line);
  background: var(--stl-surface-2);
}
.stl-dropzone.is-drag-over {
  border-color: var(--stl-accent);
  background: var(--stl-accent-soft);
  border-style: solid;
}
.stl-dropzone__inner { display: grid; gap: 6px; place-items: center; color: var(--stl-text-muted); }
.stl-dropzone__h { margin: 8px 0 0; font-size: 14px; font-weight: 500; color: var(--stl-text); }
.stl-dropzone__sub { margin: 0; font-size: 12.5px; color: var(--stl-text-subtle); }

/* Image preview — thumbnail + meta in a 2-col layout. */
.stl-add-image__preview {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: var(--s-3);
  padding: var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
}
.stl-add-image__preview[hidden] { display: none; }
.stl-add-image__preview img {
  width: 96px; height: 96px;
  object-fit: cover;
  border-radius: var(--r-sm);
  border: 1px solid var(--stl-hairline-hi);
}
.stl-add-image__meta { display: grid; gap: 4px; align-content: center; }

/* ============== SOCIAL SIGN-IN (Google / Apple) ==============
 * Sits above the email/magic-link form. The Google Identity Services
 * library injects its own branded button into #google-signin-host —
 * we own the surrounding chrome only (centering, fallback text,
 * divider to the email form). */
.stl-login__social {
  display: grid;
  justify-items: center;
  gap: 8px;
  margin-bottom: var(--s-3);
}
.stl-login__social-host {
  min-height: 44px;
  display: flex;
  justify-content: center;
}
.stl-login__social-fallback {
  font-size: 12.5px;
  color: var(--stl-text-muted);
  text-align: center;
  margin: 0;
}
.stl-login__divider {
  /* The rule is two flex tracks with the label between them, so nothing
   * has to be masked. This used to be a full-width rule with an opaque
   * --stl-bg swatch painted behind the label to punch a hole in it —
   * but the login card sits on --stl-surface with ambient orbs behind
   * that, so the swatch never matched what was actually underneath and
   * read as a grey box around the text (Josh 2026-08-11). It also hard-
   * coded the gap at calc(50% - 60px), which only holds for a label
   * this exact width: any longer string, a different font fallback or a
   * translation and the rules would run under it or leave a gap.
   *
   * Flex tracks are background-agnostic and self-sizing, so this works
   * on any surface and with any label. Use this shape for any future
   * labelled divider — don't reach for a matching-colour swatch. */
  display: flex;
  align-items: center;
  gap: 12px;
  margin: var(--s-3) 0;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--stl-text-subtle);
  font-family: var(--stl-font-mono);
}
.stl-login__divider::before,
.stl-login__divider::after {
  content: '';
  flex: 1 1 0;
  min-width: 0;
  height: 1px;
  background: var(--stl-hairline);
}
.stl-login__divider span {
  flex: 0 0 auto;
}

/* ============== STRENGTHS RADAR SECTION ==============
 * Two-column layout: chart on the left, "Focus next" list + AI-tailored
 * CTA on the right. Collapses to stacked on phones. Used on the results
 * overview (per-attempt) and on the history page (all-time aggregate).
 * Inherits the active test's --stl-tint so chart + CTA glow match. */
/* B6 layout — vertical stack. Section is the outermost wrapper that
 * holds (1) the joined chart+SW container and (2) the naked CTA below.
 * No padding or border on the section itself; the joined container
 * provides its own chrome and the CTA stands on its own. Top margin
 * is intentionally tight (8px) so the chips row above doesn't read
 * as a separate visual zone; bottom keeps a normal gap before the
 * attempts list below. */
.stl-radar-section {
  display: grid;
  gap: var(--gap-normal);
  /* No outer margin — the surrounding card's grid gap
   * (.stl-history { gap: var(--gap-normal) }) handles separation from
   * adjacent subsections. Adding margin here would compound to a
   * 40px gap between the chip row and the radar's outer border. */
  margin: 0;
}
/* Joined container — one outer card holding both the chart zone and
 * the SW zone, with a hairline divider between them. Overflow:hidden
 * on the outer card means inner zones share the corner radius
 * cleanly without needing per-zone radius overrides. */
.stl-radar-section__joined {
  background:
    radial-gradient(ellipse at top left, rgba(148, 163, 184, 0.06), transparent 60%),
    var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  overflow: hidden;
}
/* Tight padding around the chart zone — every pixel saved here grows
 * the canvas. Previously 16px on all sides; dropped to 8/12 so the
 * chart isn't drowning in margin. The Focus-next / SW zone below keeps
 * its 16px for readability of the pill row. */
.stl-radar-section__chart-zone { padding: 12px 8px 8px; }
.stl-radar-section__chart-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 4px;     /* was 8px — tighten gap below eyebrow */
  padding: 0 4px;         /* tiny side breathing room for the eyebrow */
}
.stl-radar-section__divider {
  height: 1px;
  background: var(--stl-hairline);
}
.stl-radar-section__sw-zone {
  padding: var(--s-4);
}
.stl-radar-section[hidden] { display: none; }
@media (max-width: 640px) {
  .stl-radar-section { grid-template-columns: 1fr; gap: var(--s-4); }
}

.stl-radar-section__chart-wrap {
  display: grid;
  gap: 8px;
  min-width: 0;
}
.stl-radar-section__canvas-host {
  /* Chart.js needs a sized parent for responsive: true. Aspect
   * 1:1 keeps the spider symmetrical regardless of viewport. Centered
   * inside the chart zone with a max-width so the radar doesn't
   * stretch end-to-end on huge displays (would clip its labels). Caps
   * raised so wide cards (Analysis) get a substantially bigger
   * polygon — the previous 520/420 caps left the chart looking
   * lost in the chart-zone whitespace. */
  position: relative;
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
  aspect-ratio: 1 / 1;
  max-height: 600px;
}
.stl-radar-section__canvas-host canvas {
  width: 100% !important;
  height: 100% !important;
}
/* Loading placeholder. Sits inside the canvas host, which already has
 * position: relative + a 1:1 aspect ratio — so the blank block is
 * exactly the footprint the radar is about to occupy and nothing
 * shifts when the data lands. */
/* Visually hidden but announced — the app had no such utility, and the
 * placeholder's aria-live region needs words, not just a spinner. */
.stl-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.stl-radar-section__loading {
  position: absolute;
  inset: 0;
  display: none;
  place-items: center;
  border: 1px dashed var(--stl-hairline);
  border-radius: var(--r-md);
  background: rgba(148, 163, 184, 0.03);
}
.stl-radar-section.is-loading .stl-radar-section__loading { display: grid; }
.stl-radar-section.is-loading .stl-radar-section__canvas-host canvas { visibility: hidden; }
/* Nothing to expand or focus on yet — keep the chrome quiet so the
 * placeholder reads as "loading", not as a broken chart. */
.stl-radar-section.is-loading .stl-radar-section__expand,
.stl-radar-section.is-loading .stl-radar-section__sw-zone,
.stl-radar-section.is-loading .stl-radar-section__divider { visibility: hidden; }

/* Hover tooltip for the radar's perimeter labels. Surfaces the full
 * untruncated topic name + percentage when the user moves over (or
 * near) a label whose on-canvas rendering was clipped to 14 chars
 * or visually overlapped with a neighbor. The element is appended
 * to <body> by initRadarLabelTooltips() and uses position: fixed
 * with viewport-pixel coords so it escapes the chart container's
 * overflow:hidden clip (the rounded-corner card would otherwise
 * cut off any tooltip near the edge). */
.stl-radar-label-tip {
  position: fixed;
  /* Anchor bottom-center to the label's pixel coordinates so the
   * tooltip floats above the label without covering it. */
  transform: translate(-50%, -100%);
  background: rgba(20, 22, 32, 0.97);
  border: 1px solid var(--stl-hairline-hi);
  border-radius: 8px;
  padding: 6px 10px;
  font-size: 12px;
  font-weight: 500;
  color: var(--stl-text);
  letter-spacing: -0.005em;
  white-space: nowrap;
  pointer-events: none;
  /* Lift it cleanly out of the chart canvas via a soft shadow so it
   * reads as a UI element rather than another label in the cloud. */
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5),
              0 0 0 1px rgba(0, 0, 0, 0.4);
  opacity: 0;
  transition: opacity 120ms ease;
  /* Sit above everything including the sticky header. */
  z-index: 1000;
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  /* Slight vertical nudge — sits 4px above the label anchor (the JS
   * already pushes top by 4px, this just smooths sub-pixel rendering). */
  will-change: opacity, left, top;
}
.stl-radar-label-tip.is-visible { opacity: 1; }
.stl-radar-label-tip__pct {
  color: var(--stl-text-muted);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
}

/* Expand toggle below the radar. Sits right-aligned under the canvas
 * so it reads as a chart control, not a card-level action. Hidden by
 * default; surfaced by JS when there are more topics than the cap. */
.stl-radar-section__expand {
  margin-top: 8px;
  text-align: right;
  font-size: 12px;
}
.stl-radar-section__expand[hidden] { display: none; }

/* ============ Expanded state — Show all topics ============
 * Vertical-stack layout has no aside to collapse; "Show all topics"
 * simply grows the canvas vertically so the bigger axis count has
 * room. The hairline divider + SW zone stay put below. */
.stl-radar-section__canvas-host {
  transition: max-width 380ms cubic-bezier(.22, .9, .25, 1),
              max-height 380ms cubic-bezier(.22, .9, .25, 1);
}
.stl-radar-section.is-expanded .stl-radar-section__canvas-host {
  /* Lift the centering cap so the chart fills more of the chart-zone
   * width, and grow taller so labels have breathing room around the
   * busier spider. */
  max-width: 720px;
  max-height: 620px;
}

/* .stl-radar__sw-tab* (Strengths/Weaknesses toggle) deleted 2026-08-18
 * (FIXLIST P3): zero markup anywhere — the rules never rendered. */
/* Standalone CTA — sits below the joined chart+SW container as the
 * naked primary action (no surrounding chrome — the button's gradient
 * + shadow are the surface). Full-width within its parent section. */
.stl-radar__cta--inchart {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 0;
  width: 100%;
}
.stl-radar__cta--inchart svg {
  /* Subtle pulse on the AI mark so it reads as "smart" not static.
   * 2.4s loop with a CSS-keyframe scale tween; respects reduced
   * motion via the body-level media query at the bottom of this file. */
  animation: stl-ai-pulse 2.4s ease-in-out infinite;
}
@keyframes stl-ai-pulse {
  0%, 100% { transform: scale(1); opacity: 0.95; }
  50%      { transform: scale(1.12); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .stl-radar__cta--inchart svg { animation: none; }
}

.stl-radar-section__cta {
  display: grid;
  gap: 10px;
  align-content: start;
  /* Explicit width so the collapse animation has a known starting
   * point. max-width caps it on narrow viewports. The width itself
   * is what we animate to 0 in the is-expanded state below. Sized
   * to comfortably fit 4 horizontal focus pills with their labels;
   * the pills truncate names via ellipsis on the rare super-long
   * topic. */
  width: 340px;
  max-width: 100%;
  overflow: hidden;
}
.stl-radar__focus {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 4px;
  font-size: 13px;
  color: var(--stl-text);
  /* Crossfade during test-type swaps is driven by JS: a clone of the
   * current list is overlaid on top during the swap, then faded out
   * while the live element shows the new content underneath. See
   * renderHistoryRadarFocusInto. No CSS state needed here. */
}
/* Row variant — horizontal pill strip used by the dual Strengths/
 * Weaknesses aside. Each pill flex-grows to share the row width
 * equally so items are visually balanced, and shrinks via min-width:0
 * so long topic names truncate with ellipsis rather than wrapping. */
.stl-radar__focus--row {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  width: 100%;
}
.stl-radar__focus--row .stl-radar__focus-item {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 4px;
  padding: 8px 10px;
  text-align: left;
}
.stl-radar__focus--row .stl-radar__focus-name {
  font-size: 12px;
  width: 100%;
}
.stl-radar__focus--row .stl-radar__focus-pct {
  font-size: 14px;
}
.stl-radar__focus-item {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  padding: 6px 10px;
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--stl-hairline);
}
.stl-radar__focus-name {
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Dual-section block layout: Strengths first, then Weaknesses below.
 * Each block is a label + horizontal pill row. The label sits flush
 * left, smaller than the focus item content, mono-typed so it reads
 * as a section heading. */
.stl-radar__focus-block + .stl-radar__focus-block {
  margin-top: 14px;
}
/* Focus block label inherits .stl-eyebrow styling (font, color, dot).
 * Only override what differs — bump letter-spacing slightly and add
 * a bottom margin since the eyebrow itself has margin: 0. */
.stl-radar__focus-label {
  margin: 0 0 6px;
  /* tracking now inherited from .stl-eyebrow */
}
/* Color-tint the % for each block so a glance distinguishes the two
 * lists without needing to re-read the section label. */
.stl-radar__focus-block--strengths .stl-radar__focus-pct {
  color: var(--stl-green);
}
.stl-radar__focus-block--weaknesses .stl-radar__focus-pct {
  color: var(--stl-red);
}
/* Tint the leading eyebrow dot to match each block's identity — green
 * for strengths, red for weaknesses — with a matching soft glow.
 * Overrides the global accent-tinted dot from .stl-eyebrow::before. */
.stl-radar__focus-block--strengths .stl-radar__focus-label::before {
  background: var(--stl-green);
  box-shadow: 0 0 8px rgba(52, 211, 153, 0.55);
}
.stl-radar__focus-block--weaknesses .stl-radar__focus-label::before {
  background: var(--stl-red);
  box-shadow: 0 0 8px rgba(var(--stl-red-rgb), 0.55);
}
.stl-radar__focus-pct {
  font-family: var(--stl-font-mono);
  font-size: 12px;
  color: rgba(var(--stl-tint, 139 134 255), 0.95);
  font-weight: 600;
}
.stl-radar__cta {
  width: 100%;
  margin-top: 6px;
}
.stl-radar__cta-hint {
  font-size: 12px;
  color: var(--stl-text-muted);
  margin: 0;
  line-height: 1.45;
  /* Center under the full-width AI CTA on both History and Analysis
   * pages — visually anchors the supporting text to the button above. */
  text-align: center;
}

/* .stl-review-tab* (results review bucket tabs) deleted 2026-08-18
 * (FIXLIST P3): zero markup anywhere; the block also carried an off-token
 * palette and a rule-2 selected ring — bad examples waiting to be copied. */
/* Upload Image — tab pills that switch between Convert (single
 * extraction) and Generate similar (multi-question generation).
 * Pure CSS pill row; active state via .is-active. */
.stl-add-image__tabs {
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-pill);
  margin-bottom: var(--s-2);
  align-self: start;
}
.stl-add-image__tab {
  appearance: none; -webkit-appearance: none;
  background: transparent;
  border: 0;
  padding: 6px 14px;
  border-radius: var(--r-pill);
  font: inherit;
  font-size: 13px;
  color: var(--stl-text-muted);
  cursor: pointer;
  transition: background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.stl-add-image__tab:hover { color: var(--stl-text); }
.stl-add-image__tab.is-active,
.stl-add-image__tab[aria-selected="true"] {
  background: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.18);
  color: var(--stl-text);
}
.stl-add-image__step[hidden] { display: none; }

/* JSON import preview / error list. Compact panel that surfaces parse
 * + validation errors per row so the admin can fix and re-paste. */
.stl-add-json__preview {
  padding: var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
}
.stl-add-json__errors { display: grid; gap: 4px; margin-top: 6px; }
.stl-add-json__error { color: var(--stl-red); }

/* Radio row for the difficulty-mode selector in the Similar-from-image
 * flow. Stacks on narrow viewports, sits on a single row on wide ones. */
.stl-radio-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-4);
  margin-bottom: var(--s-2);
}
.stl-radio {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13.5px;
  color: var(--stl-text);
  cursor: pointer;
  user-select: none;
}
.stl-radio input[type="radio"] {
  margin: 0;
  accent-color: var(--stl-accent);
}

/* Source-question reference in the post-generation preview list.
 * A compact panel with the original image inline so admins can spot
 * the AI's "similar but not too similar" calibration at a glance. */
.stl-add-sim__source {
  display: grid;
  gap: 8px;
  padding: var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  margin-bottom: var(--s-3);
}
.stl-add-sim__source img {
  max-width: 100%;
  max-height: 240px;
  object-fit: contain;
  border-radius: var(--r-sm);
  border: 1px solid var(--stl-hairline-hi);
  background: rgba(255, 255, 255, 0.02);
  display: block;
}
.stl-add-image__name {
  margin: 0;
  font-family: var(--stl-font-mono);
  font-size: 12px;
  color: var(--stl-text);
  word-break: break-all;
}

.stl-add-image__status {
  display: flex; align-items: center; gap: 10px;
  padding: 10px var(--s-3);
  border-radius: var(--r-md);
  background: var(--stl-accent-soft);
  border: 1px solid var(--stl-accent-line);
  color: var(--stl-text);
  font-size: 13px;
}
.stl-add-image__status[hidden] { display: none; }
.stl-add-image__spinner {
  /* Geometry only — the ring itself comes from the shared .stl-loader
   * rule below. This used to be its own border-ring spinner with an
   * accent top arc, spinning at a different speed. */
  --stl-loader-size: 14px;
  --stl-loader-thickness: 1.3px;
}
@keyframes stl-spin { to { transform: rotate(360deg); } }

/* =============================================================
 *  Brand loading indicator — used wherever we're hitting the API
 * =============================================================
 *
 * Design: a conic-gradient rainbow arc rotating in a ring. Picks up
 * the same hue stops as the primary CTA (--stl-rainbow) so loading
 * reads as "the brand is working" rather than a generic spinner.
 *
 * Implementation:
 *   • A 1.6px-thick ring (mask + radial-gradient hollow it out) sized
 *     by --stl-loader-size (default 18px; override on a per-callsite
 *     basis with the CSS var).
 *   • conic-gradient runs sky → violet → pink → transparent, so the
 *     "tail" fades back into the background instead of a hard seam.
 *   • The whole element rotates on stl-spin; the conic gradient is
 *     drawn once and the rotation is GPU-cheap.
 *
 * Size variants:
 *   .stl-loader--sm  → 14px (inline next to text inside a button)
 *   .stl-loader      → 18px (default; default in a chip / toast)
 *   .stl-loader--lg  → 32px (mid-modal splash, bank-loader)
 *   .stl-loader--xl  → 56px (full-screen boot splash)
 *
 * Modifier:
 *   .stl-loader--solid  forces a solid dark hub (use when sitting on
 *                       a non-dark background; the default lets the
 *                       parent show through the hub so the arc looks
 *                       cleanly cut out).
 */
.stl-loader,
.stl-boot-veil__spinner,
.stl-add-image__spinner,
.stl-btn.is-loading::after {
  /* ==================================================================
   * THE spinner. There is one.
   * ==================================================================
   * The app had four loading indicators wearing three different looks:
   * this gradient ring, a near-copy inside .stl-btn.is-loading::after
   * with hardcoded geometry that made it visibly fatter, and two plain
   * border rings with an accent-colored top arc
   * (.stl-boot-veil__spinner across 9 markup sites, and
   * .stl-add-image__spinner across 3). Which one you saw depended on
   * which corner of the app you were in (Josh 2026-08-11: "make sure
   * ALL loading spinners being used are the same").
   *
   * The selector list below is the whole fix: every loading indicator
   * now resolves to this rule, and each variant contributes nothing but
   * its two size variables. The var() fallbacks matter — a variant
   * declared EARLIER in this file than this rule cannot set the
   * defaults here (equal specificity, later wins), so the defaults live
   * in the fallbacks and the variants only ever override.
   *
   * Stroke is 20% thinner than it was at every size, per Josh:
   * "slightly thinner in stroke width". Uniformly scaled rather than
   * re-derived from one ratio — the old sizes were deliberately not
   * proportional (the 56px ring was already relatively finer than the
   * 14px one), and flattening them to a single ratio made the big ones
   * FATTER, which is the opposite of the ask.
   * ================================================================== */
  display: inline-block;
  flex: 0 0 auto;
  width: var(--stl-loader-size, 18px);
  height: var(--stl-loader-size, 18px);
  border-radius: 50%;
  background:
    conic-gradient(
      from 0deg,
      transparent 0deg,
      #86efac 60deg,
      #fde047 140deg,
      #fb923c 220deg,
      var(--stl-red-text) 300deg,
      transparent 360deg
    );
  /* Hollow out the centre so we have a ring, not a disc. The mask
   * is a radial-gradient with a hard cutoff at the inner radius. */
  -webkit-mask:
    radial-gradient(circle,
      transparent calc(50% - var(--stl-loader-thickness, 1.6px)),
      #000 calc(50% - var(--stl-loader-thickness, 1.6px) + 0.5px)
    );
          mask:
    radial-gradient(circle,
      transparent calc(50% - var(--stl-loader-thickness, 1.6px)),
      #000 calc(50% - var(--stl-loader-thickness, 1.6px) + 0.5px)
    );
  animation: stl-spin 900ms linear infinite;
  vertical-align: middle;
}
.stl-loader--sm  { --stl-loader-size: 14px; --stl-loader-thickness: 1.3px; }
.stl-loader--lg  { --stl-loader-size: 32px; --stl-loader-thickness: 2.4px; }
.stl-loader--xl  { --stl-loader-size: 56px; --stl-loader-thickness: 3.2px; }

/* "Inside button" placement — sits where the text was, gets a margin
 * to keep optical balance. Pairs with the .is-loading state below. */
.stl-btn .stl-loader {
  margin-right: 8px;
}
.stl-btn--icon .stl-loader { margin-right: 0; }

/* .is-loading on any .stl-btn swaps the label for the spinner without
 * the button changing width — we hold the original width on the click
 * site in JS (setButtonLoading) and add this class. Visible label is
 * faded out so the spinner takes its place. */
.stl-btn.is-loading {
  pointer-events: none;
  position: relative;
  /* Slight desaturation so a loading primary doesn't compete with
   * surrounding interactive surfaces. */
  filter: saturate(0.92);
}
.stl-btn.is-loading > .stl-btn__label,
.stl-btn.is-loading > .stl-btn__label-text {
  opacity: 0;
}
/* ...but most buttons are raw text with maybe an icon, no label span at
 * all, and setButtonLoading does not wrap them. Those kept their label
 * fully visible with the spinner drawn on top of it. Hide the button's
 * own text via a transparent color (the ring is a background gradient,
 * so it is unaffected) and fade any element children, which covers
 * icons and label spans alike without requiring markup changes. */
.stl-btn.is-loading {
  /* !important because button variants win on specificity: light mode's
   * `:root[data-theme="light"] .stl-btn--pill-rainbow` is (0,3,0)
   * against this rule's (0,2,0), so without it a loading pill-rainbow
   * button kept its label and the ring painted straight over the word.
   * It showed in light mode only, which is exactly the kind of thing
   * that ships. */
  color: transparent !important;
}
.stl-btn.is-loading > * {
  opacity: 0;
}
/* When we don't have a wrapper around the label (most buttons just
 * have raw text + maybe an icon), use a ::before-rendered spinner
 * absolutely positioned. JS adds .is-loading; the pseudo-element
 * inherits the same conic gradient as .stl-loader. */
.stl-btn.is-loading::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  pointer-events: none;
  /* Geometry only — gradient, mask and animation come from the shared
   * .stl-loader rule. It used to restate all three with a hardcoded
   * 4.4px/5px mask, which worked out to roughly a 3.2px stroke on a
   * 16px ring: visibly fatter than every other spinner in the app. */
  --stl-loader-size: 16px;
  --stl-loader-thickness: 1.4px;
}
@keyframes stl-boot-splash-in {
  to { opacity: 1; }
}
@keyframes stl-boot-splash-out {
  to { opacity: 0; }
}

/* Reduced motion: keep the spinner visible but stop the rotation
 * and the splash animation. */
@media (prefers-reduced-motion: reduce) {
  .stl-loader,
  .stl-btn.is-loading::after {
    animation: none;
  }
}

/* ─── Generate-with-AI form + preview ─── */
.stl-checkbox-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px var(--s-3);
  margin-top: 4px;
}
@media (min-width: 720px) {
  .stl-checkbox-grid { grid-template-columns: repeat(3, 1fr); }
}
.stl-checkbox {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: var(--stl-text);
  cursor: pointer;
}
.stl-checkbox input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--stl-accent);
  cursor: pointer;
}
.stl-label--sub {
  font-size: 11px;
  color: var(--stl-text-muted);
}

.stl-add-gen__preview-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: var(--s-3);
}
.stl-add-gen__list {
  display: grid;
  gap: var(--s-3);
  max-height: 460px;
  overflow-y: auto;
  padding-right: 4px;
}
.stl-add-gen__item {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-3);
  padding: var(--s-3);
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-sm);
  transition: border-color var(--dur-fast) var(--ease-spring),
              background var(--dur-fast) var(--ease-spring);
}
.stl-add-gen__item.is-unchecked {
  opacity: 0.55;
}
.stl-add-gen__item.is-duplicate {
  border-color: rgba(var(--stl-red-rgb), 0.4);
  background: rgba(var(--stl-red-rgb), 0.06);
}
.stl-add-gen__item-check {
  align-self: start;
  margin-top: 4px;
}
.stl-add-gen__item-check input {
  width: 18px;
  height: 18px;
  accent-color: var(--stl-accent);
  cursor: pointer;
}
.stl-add-gen__item-meta {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 6px;
  font-size: 12px;
  color: var(--stl-text-muted);
}
.stl-add-gen__item-meta strong {
  color: var(--stl-text);
  font-weight: 500;
}
.stl-add-gen__item-stem {
  font-size: 14px;
  line-height: 1.5;
  color: var(--stl-text);
  margin: 0 0 8px;
}
.stl-add-gen__item-choices {
  display: grid;
  gap: 4px;
  font-size: 13px;
  color: var(--stl-text-muted);
}
.stl-add-gen__item-choice.is-correct {
  color: var(--stl-green);
  font-weight: 500;
}
.stl-add-gen__item-explain {
  margin: 8px 0 0;
  padding-top: 8px;
  border-top: 1px dashed var(--stl-hairline);
  font-size: 12px;
  line-height: 1.5;
  color: var(--stl-text-muted);
}
.stl-add-gen__item-flag {
  display: inline-block;
  margin-top: 6px;
  padding: 2px 8px;
  background: rgba(var(--stl-red-rgb), 0.12);
  color: var(--stl-red);
  border-radius: var(--r-pill);
  font-size: 11px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

@media (max-width: 720px) {
  .stl-add-picker { grid-template-columns: 1fr; }
}

/* ============== auth: header pill + login screen ==============
 * The login + 401 screens use a distinct "auth card" treatment:
 * compact, centered on the viewport, with a soft accent halo behind
 * it. Visually related to the test screens (same dark canvas, same
 * Geist type, lavender accent) but unmistakeably its own moment —
 * a panel rather than a full-page hero card.
 *
 * Header pill (right side): user email + role badge when signed in,
 * Sign in button when signed out.
 */
.stl-header__auth { display: inline-flex; align-items: center; gap: var(--s-2); }
.stl-header__auth[hidden] { display: none; }

/* User menu — trigger pill (email + chevron) that opens a dropdown
 * panel anchored below. Designed to absorb more menu items over
 * time (Settings, Profile, etc.) — the current panel has the role
 * label at the top and Sign out below; new items slot in between. */
.stl-header__usermenu {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.stl-header__usermenu[hidden] { display: none; }

.stl-header__usermenu-trigger {
  appearance: none; -webkit-appearance: none;
  font: inherit; cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px 5px 12px;
  border-radius: var(--r-pill);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface-2);
  color: var(--stl-text-muted);
  transition:
    background var(--dur-base) ease,
    border-color var(--dur-base) ease,
    color var(--dur-base) ease;
}
.stl-header__usermenu-trigger:hover {
  background: var(--stl-surface-3);
  border-color: var(--stl-hairline-hi);
  color: var(--stl-text);
}
.stl-header__usermenu-trigger:focus-visible {
  outline: 2px solid rgba(var(--stl-rainbow-mid-rgb), 0.9);
  outline-offset: 2px;
}
.stl-header__usermenu-trigger[aria-expanded="true"] {
  color: var(--stl-text);
  border-color: var(--stl-hairline-hi);
}
.stl-header__user-email {
  font-family: var(--stl-font-mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
  /* Truncate long emails on narrow screens. */
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.stl-header__usermenu-chev {
  flex-shrink: 0;
  opacity: 0.7;
  transition: transform var(--dur-base) ease, opacity var(--dur-base) ease;
}
.stl-header__usermenu-trigger:hover .stl-header__usermenu-chev,
.stl-header__usermenu-trigger[aria-expanded="true"] .stl-header__usermenu-chev {
  opacity: 1;
}
.stl-header__usermenu-trigger[aria-expanded="true"] .stl-header__usermenu-chev {
  transform: rotate(180deg);
}
/* Compact account glyph. Hidden on desktop (the email reads instead);
 * the ≤1024px responsive block swaps it in for the email + chevron so
 * the trigger collapses to a single user icon when the header is tight. */
.stl-header__user-icon {
  display: none;
  align-items: center;
  justify-content: center;
}
.stl-header__user-icon svg { display: block; }

/* Dropdown panel — anchored under the trigger, opens downward. */
.stl-header__usermenu-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 200px;
  padding: 6px;
  /* Frosted glass (2026-08-18): was var(--stl-surface-2) — 5.5% white,
   * so the page bled through the menu. Floating panels use --stl-panel
   * + backdrop blur; see the token's comment. */
  background: var(--stl-panel);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--stl-hairline-hi);
  border-radius: 10px;
  box-shadow: 0 16px 36px -16px rgba(0, 0, 0, 0.7),
              0 4px 12px -6px rgba(0, 0, 0, 0.5);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* Light-mode override — --stl-surface-2 in light mode is rgba(15, 23, 42, 0.045),
 * which is effectively transparent. That works fine in dark mode (the tint
 * lifts slightly off the near-black canvas) but on a near-white page the
 * menu has no surface of its own and the navbar reads through. Force an
 * opaque white panel + soften the shadow (the 70% black drop was tuned
 * for dark mode and reads too heavy on light). */
:root[data-theme="light"] .stl-header__usermenu-panel {
  border-color: var(--stl-hairline-hi);
  box-shadow: 0 16px 36px -16px rgba(15, 23, 42, 0.18),
              0 4px 12px -6px rgba(15, 23, 42, 0.10);
}
.stl-header__usermenu-panel[hidden] { display: none; }
/* Portaled instance — when the panel is moved to <body> on open, it
 * escapes the header's stacking context (the header carries
 * backdrop-filter, which traps descendants regardless of their
 * z-index). JS sets position: fixed + top + right inline; this rule
 * bumps the z-index well above the header's 50 AND above other
 * body-level overlays (1000 for the math tooltip on line 6423) so
 * the panel paints over the navbar instead of behind it. Previous
 * value of 200 was getting clipped — bump to 1500 so it sits below
 * the boot splash (9999) and full-screen modals but above everything
 * navbar-related. */
.stl-header__usermenu-panel--portaled {
  z-index: 1500;
}

/* Tiny header inside the panel — repurposed from the old role tag.
 * No longer has the green admin-status pill chrome (the active
 * Admin nav tab is enough indication that the user is an admin). */
.stl-header__usermenu-label {
  font-family: var(--stl-font-mono);
  font-size: 9.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  padding: 8px 10px 6px;
  border-bottom: 1px solid var(--stl-hairline);
  margin-bottom: 4px;
}
.stl-header__usermenu-label:empty { display: none; }

.stl-header__usermenu-item {
  appearance: none; -webkit-appearance: none;
  font: inherit; cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--stl-text);
  font-size: 13px;
  font-weight: 500;
  text-align: left;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.stl-header__usermenu-item:hover {
  background: var(--stl-surface-3);
}
.stl-header__usermenu-item:focus-visible {
  outline: 2px solid rgba(var(--stl-rainbow-mid-rgb), 0.6);
  outline-offset: -2px;
}
.stl-header__usermenu-item svg {
  flex-shrink: 0;
  opacity: 0.8;
}

/* Admin "View as" segmented control (account dropdown, admin-only).
 * Three equal segments; the active one lifts onto the accent tint.
 * Sits between the role label and the sign-out item. */
.stl-usermenu-viewas {
  padding: 6px 10px 8px;
  border-bottom: 1px solid var(--stl-hairline);
  margin-bottom: 4px;
}
.stl-usermenu-viewas[hidden] { display: none; }
.stl-usermenu-viewas__lbl {
  display: block;
  font-family: var(--stl-font-mono);
  font-size: 9.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  margin-bottom: 6px;
}
/* 2×2, not 1×4 (Joshua 2026-08-16: added "First time"). Four segments on
 * one row inside a 200px-min account menu gives each about 45px, which
 * cuts "First time" in half. Two rows keep every label whole and the hit
 * targets honest.
 *
 * Pinned to 2 rather than `auto-fit`, which was the first attempt: auto-fit
 * derives the column count from the panel's actual width, and the panel is
 * `min-width: 200px` with no max — a longer signed-in email widens it, at
 * which point three segments fit and the fourth is orphaned on its own
 * row. A layout that depends on how long someone's email address is, is
 * not a layout. A fifth view later means changing this 2 on purpose. */
.stl-usermenu-viewas__seg {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 3px;
  padding: 3px;
  border-radius: 8px;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
}
/* The sandbox reassurance, shown only while First time is selected. */
.stl-usermenu-viewas__note {
  margin: 6px 0 0;
  font-size: 10.5px;
  line-height: 1.4;
  color: var(--stl-text-subtle);
}
.stl-usermenu-viewas__note[hidden] { display: none; }
:root[data-theme="light"] .stl-usermenu-viewas__seg {
  background: rgba(15, 23, 42, 0.04);
}
.stl-viewseg {
  appearance: none; -webkit-appearance: none;
  font: inherit; cursor: pointer;
  border: 0;
  border-radius: 6px;
  padding: 5px 6px;
  background: transparent;
  color: var(--stl-text-muted);
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.stl-viewseg:hover { color: var(--stl-text); }
.stl-viewseg.is-active,
.stl-viewseg[aria-checked="true"] {
  /* FIXLIST P2 (2026-08-18): dropped the inset ring (rule 2) and added the
   * ARIA hook beside the class so visual and announced state cannot drift. */
  background: var(--stl-accent-soft);
  color: var(--stl-accent);
  border-color: var(--stl-accent-line);
}
.stl-viewseg:focus-visible {
  outline: 2px solid rgba(var(--stl-rainbow-mid-rgb), 0.6);
  outline-offset: -2px;
}

/* When the auth screens are mounted, center vertically on the viewport.
 * `.stl-main:has(...)` is supported in all current browsers; pre-Chromium
 * browsers fall back to top-aligned, which is not catastrophic. */
.stl-main:has([data-screen="login"]:not([hidden])),
.stl-main:has([data-screen="forbidden"]:not([hidden])) {
  align-items: center;
  justify-items: center;
  /* Optically center the auth panel. This used to be
   * `min-height: calc(100dvh - 60px)`, which reserved the viewport
   * minus the header and forgot the footer, so login and forbidden
   * ALWAYS carried 122px of dead scroll below the fold. main now grows
   * to fill body's column instead, so centering is just alignment. */
  align-content: center;
}

/* The screen wrapper centers the card horizontally too, even on
 * browsers that don't support :has(). */
.stl-screen--auth {
  display: grid;
  place-items: center;
  width: 100%;
}

/* --- auth card --- */
.stl-auth-card {
  position: relative;
  width: 100%;
  max-width: var(--shell-focus);
  padding: var(--s-7) var(--s-6) var(--s-6);
  border-radius: 18px;
  /* Layered surface — a thin top-light gradient over a dark panel,
   * with a soft lavender halo bleeding from the top. Distinct from
   * the test cards' .stl-card--hero treatment (which uses a 165deg
   * accent wash + heavier shadow for stage presence). */
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.01) 30%, transparent),
    rgba(13, 14, 24, 0.78);
  border: 1px solid var(--stl-hairline-hi);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 24px 60px -16px rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  display: grid;
  gap: var(--s-5);
}
/* Accent halo behind the card. Sits in a pseudo so it can sit
 * underneath the card with a soft glow leaking out the top. */
.stl-auth-card::before {
  content: '';
  position: absolute;
  inset: -60px -40px auto -40px;
  height: 200px;
  background: radial-gradient(closest-side, rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.22), transparent 70%);
  filter: blur(40px);
  pointer-events: none;
  z-index: -1;
}
/* Light mode — the dark panel + white top-light gradient above reads as
 * a broken dark card on the white page. Flip to a white surface with a
 * faint slate top-light and a soft ambient shadow so it matches the rest
 * of the light theme. The accent halo (::before) stays — it tints from
 * --stl-tint and works on either field. */
:root[data-theme="light"] .stl-auth-card {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.6) 30%, transparent),
    #ffffff;
  border: 1px solid var(--stl-hairline);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.8),
    0 24px 60px -20px rgba(15, 23, 42, 0.22);
}

.stl-auth-brand {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.stl-auth-card__body { display: grid; gap: var(--s-4); }
.stl-auth-h1 {
  margin: 0;
  font-size: 1.5rem;
  letter-spacing: -0.02em;
  font-weight: 600;
  line-height: 1.15;
  color: var(--stl-text);
}
.stl-auth-sub {
  margin: 0;
  color: var(--stl-text-muted);
  font-size: 0.95rem;
  line-height: 1.5;
}
.stl-auth-card__actions {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  margin-top: var(--s-1);
}
.stl-auth-card__actions .stl-btn { width: 100%; }

/* Login form internals */
.stl-login__form { display: grid; gap: var(--s-3); }
.stl-login__form[hidden] { display: none; }

/* Submit button has two stacked labels — one for "idle", one for
 * "sent" — and a data-state attribute on the button drives which
 * one is visible. The labels slide vertically (idle goes up & out,
 * sent comes up from below) for a more confident state shift than
 * a flat crossfade. */
.stl-login__submit { position: relative; overflow: hidden; }

/* Hero-scale override for the rainbow-border pill. --pill-rainbow ships
 * navbar-compact sizing (6px padding, 12.5px text, min-height 0) and it
 * is declared AFTER .stl-btn--hero, so a bare hero class loses the cascade
 * on those three properties. Restating them at .stl-login__submit
 * .stl-btn--pill-rainbow specificity puts the hero scale back. Same move
 * .stl-radar__cta.stl-btn--pill-rainbow makes for the radar CTA. */
.stl-login__submit.stl-btn--pill-rainbow {
  width: 100%;
  padding: 14px 20px;
  font-size: 15px;
  min-height: 52px;
  border-radius: var(--r-pill);
  font-weight: 600;
}
.stl-login__submit.stl-btn--pill-rainbow:hover {
  /* Heavier glow to match the button's hero weight. */
  box-shadow: 0 14px 32px -12px rgba(var(--stl-rainbow-mid-rgb), 0.5);
}
.stl-login__submit-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  transition: opacity   240ms var(--ease-spring),
              transform 280ms var(--ease-spring);
}
.stl-login__submit-label[data-when="sent"] {
  position: absolute;
  inset: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  /* Starts below the label slot, ready to slide up */
  transform: translateY(14px);
}
.stl-login__submit[data-state="sent"] .stl-login__submit-label[data-when="idle"] {
  opacity: 0;
  /* Slides up and out the top */
  transform: translateY(-14px);
}
.stl-login__submit[data-state="sent"] .stl-login__submit-label[data-when="sent"] {
  opacity: 1;
  transform: translateY(0);
}

/* Checkmark draws in via stroke-dashoffset. Path length ≈ 22.6, padded
 * to 26. A small delay lets the green wash and label slide settle
 * before the line traces. */
.stl-login__submit-label[data-when="sent"] svg path {
  stroke-dasharray: 26;
  stroke-dashoffset: 26;
  transition: stroke-dashoffset 360ms var(--ease-spring) 120ms;
}
.stl-login__submit[data-state="sent"] .stl-login__submit-label[data-when="sent"] svg path {
  stroke-dashoffset: 0;
}

@media (prefers-reduced-motion: reduce) {
  .stl-login__submit-label,
  .stl-login__submit-label[data-when="sent"] svg path {
    transition: opacity 120ms linear;
    transform: none !important;
    stroke-dashoffset: 0 !important;
  }
}

/* The button itself transitions from lavender gradient to a green
 * "sent" gradient. Border, color, and shadow all morph in unison
 * for the full state-change feel. */
.stl-login__submit {
  transition:
    background  300ms var(--ease-out),
    border-color 300ms var(--ease-out),
    color       300ms var(--ease-out),
    box-shadow  300ms var(--ease-out);
}
.stl-login__submit[data-state="sent"] {
  background: linear-gradient(140deg, #4ade80 0%, var(--stl-green) 100%);
  border-color: rgba(var(--stl-green-rgb), 0.6);
  color: #052e16;
  /* Gentle outer glow telegraphs success. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.18),
    0 0 0 4px rgba(var(--stl-green-rgb), 0.18);
  /* Disabled-state visual: still readable, no opacity dim. */
  opacity: 1;
  filter: none;
  cursor: default;
}
.stl-login__submit[data-state="sent"]:hover {
  /* Override the .stl-btn--primary hover brighten while in sent state. */
  filter: none;
  background: linear-gradient(140deg, #4ade80 0%, var(--stl-green) 100%);
}

/* Sent state vs the rainbow pill. Two things need restating here.
 * 1. Specificity: `:root[data-theme="light"] .stl-btn--pill-rainbow`
 *    outscores a bare `.stl-login__submit[data-state="sent"]`, so in
 *    light mode the white padding-box fill would win and the success
 *    state would come out white instead of green. Scoping the sent
 *    rules under :root evens the score, and the light-mode selector
 *    below takes it outright.
 * 2. Layers: the green is a SINGLE background layer on purpose. It
 *    replaces the pill's padding-box + border-box pair, which drops
 *    the rainbow, and border-color then paints the (still transparent)
 *    1.5px border green. That is the intended morph, not an oversight.
 * The pill's hover lift is cancelled too — once sent, the button is
 * inert and should not respond like it is still clickable. */
:root .stl-login__submit[data-state="sent"],
:root[data-theme="light"] .stl-login__submit[data-state="sent"] {
  background: linear-gradient(140deg, #4ade80 0%, var(--stl-green) 100%);
  border-color: rgba(var(--stl-green-rgb), 0.6);
  color: #052e16;
}
:root .stl-login__submit[data-state="sent"]:hover,
:root[data-theme="light"] .stl-login__submit[data-state="sent"]:hover {
  background: linear-gradient(140deg, #4ade80 0%, var(--stl-green) 100%);
  transform: none;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.18),
    0 0 0 4px rgba(var(--stl-green-rgb), 0.18);
}
@media (prefers-reduced-motion: reduce) {
  .stl-login__submit, .stl-login__submit-label { transition: none; }
}

/* Locked input — once the email is sent, the field is read-only.
 * Lavender accent border tells the user "this is what we sent to,
 * and you can edit it via the button below." A one-shot ring pulse
 * fires on the transition into readonly, confirming the lock. */
.stl-input[readonly] {
  background: var(--stl-accent-soft);
  border-color: var(--stl-accent-line);
  color: var(--stl-text);
  animation: stl-input-lock 520ms var(--ease-spring);
}
.stl-input[readonly]:focus,
.stl-input[readonly]:focus-visible {
  border-color: var(--stl-accent);
  box-shadow: 0 0 0 4px var(--stl-accent-soft);
}
@keyframes stl-input-lock {
  0%   { box-shadow: 0 0 0 0   rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.0); }
  40%  { box-shadow: 0 0 0 8px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.22); }
  100% { box-shadow: 0 0 0 0   rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.0); }
}
@media (prefers-reduced-motion: reduce) {
  .stl-input[readonly] { animation: none; }
}

/* Post-send block — sits below the form once the link is sent.
 * Animates in (fade + slide-down) with a stagger on its children
 * so the success state assembles itself rather than just appearing. */
.stl-login__post {
  display: grid;
  gap: var(--s-3);
  margin-top: var(--s-2);
}
.stl-login__post[hidden] { display: none; }
.stl-login__post.is-revealed {
  animation: stl-post-fade-in 360ms var(--ease-spring) both;
}
.stl-login__post.is-revealed > * {
  animation: stl-post-child-in 320ms var(--ease-spring) both;
}
.stl-login__post.is-revealed > *:nth-child(1) { animation-delay:  80ms; }
.stl-login__post.is-revealed > *:nth-child(2) { animation-delay: 140ms; }
.stl-login__post.is-revealed > *:nth-child(3) { animation-delay: 200ms; }
.stl-login__post.is-revealed > *:nth-child(4) { animation-delay: 260ms; }
@keyframes stl-post-fade-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes stl-post-child-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .stl-login__post.is-revealed,
  .stl-login__post.is-revealed > * { animation: none; }
}

.stl-login__sent-tip {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--stl-text-muted);
}
/* Email rendered as a pill in the tip line so it stands out for
 * typo-eyeballing. Mono font helps with `0` vs `O` confusion. */
.stl-login__sent-email {
  display: inline-block;
  margin: 0 2px;
  padding: 1px 8px;
  border-radius: var(--r-pill);
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline-hi);
  font-family: var(--stl-font-mono);
  font-size: 12.5px;
  color: var(--stl-text);
  font-variant-numeric: tabular-nums;
}
.stl-login__sent-tip code {
  font-family: var(--stl-font-mono);
  font-size: 12px;
  color: var(--stl-text-muted);
  background: var(--stl-surface-2);
  padding: 1px 6px;
  border-radius: var(--r-sm);
}
.stl-login__sent-tip strong { color: var(--stl-text); font-weight: 500; }
.stl-login__sent-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-2);
  margin-top: var(--s-2);
}
@media (max-width: 440px) {
  .stl-login__sent-actions { grid-template-columns: 1fr; }
}

.stl-login__demo {
  padding: var(--s-4);
  border-radius: var(--r-md);
  border: 1px dashed rgba(var(--stl-yellow-rgb), 0.35);
  background: rgba(var(--stl-yellow-rgb), 0.05);
}
.stl-login__demo[hidden] { display: none; }
.stl-login__demo .stl-btn { width: 100%; }
.stl-login__mode {
  margin: 0;
  text-align: center;
  font-family: var(--stl-font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-faint);
  padding-top: var(--s-3);
  border-top: 1px solid var(--stl-hairline);
}
.stl-login__mode[hidden] { display: none; }

/* When the hint is in error state, switch its color to red. */
.stl-hint.is-error { color: var(--stl-red); }

/* On phones the header gets crowded fast — hide the user email,
 * keep the role badge + Sign in/out. */
@media (max-width: 640px) {
  .stl-header__user-email { display: none; }
  .stl-header__user { padding-left: var(--s-2); }
}

/* ============== shared-attempt banner ==============
 * Visible on the results screen when ?a= loaded a friend's payload.
 * Lavender wash matches the accent so it reads as informational, not
 * an error. Fits above the eyebrow without disturbing the bento. */
.stl-shared-banner {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
  padding: 10px var(--s-4);
  margin-bottom: var(--s-3);
  border: 1px solid var(--stl-accent-line);
  border-radius: var(--r-md);
  background: var(--stl-accent-soft);
}
.stl-shared-banner[hidden] { display: none; }
.stl-shared-banner__dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--stl-accent);
  box-shadow: 0 0 8px var(--stl-accent);
}
.stl-shared-banner__text {
  flex: 1;
  font-size: 14px;
  color: var(--stl-text);
}
.stl-shared-banner .stl-btn { padding: 7px 12px; font-size: 13px; }

/* ============== toast =====================================
 * Lightweight transient confirmation for "Link copied", "Downloaded",
 * etc. Lives at the bottom-center, fades up + in. aria-live=polite
 * announces it to screen readers without stealing focus. */
.stl-toast {
  position: fixed;
  left: 50%;
  bottom: max(var(--s-6), env(safe-area-inset-bottom));
  transform: translate(-50%, 12px);
  z-index: 100;
  padding: 10px 18px;
  border-radius: var(--r-pill);
  /* Dark pill in dark mode. It was "intentionally dark in BOTH themes"
   * until 2026-08-18, when Joshua ruled the opposite (FIXLIST D3): the
   * toast themes with the page — light mode gets its own treatment in
   * the override right below this block. The dark values stay literal
   * on purpose: this is the dark-mode look, not a token consumer. */
  background: rgba(20, 22, 36, 0.95);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: #f0f2f7;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -0.005em;
  box-shadow: 0 12px 36px -10px rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity 200ms var(--ease-out),
              transform 200ms var(--ease-out);
  pointer-events: none;
}
.stl-toast.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}
.stl-toast[hidden] { display: none; }
/* Light-mode toast (FIXLIST D3, Joshua 2026-08-18: "toasts should have
 * unique colors based on light/dark mode"). Near-white pill, dark text,
 * slate hairline; the shadow does the lifting that the dark pill's
 * contrast did. Values literal for the same reason as the dark ones. */
:root[data-theme="light"] .stl-toast {
  background: rgba(255, 255, 255, 0.96);
  border-color: rgba(15, 23, 42, 0.16);
  color: #16181f;
  box-shadow: 0 12px 36px -10px rgba(15, 23, 42, 0.28);
}
/* Tones (FIXLIST P4): a coloured left edge + tinted border, not a full
 * repaint — the pill stays readable and the tone reads at a glance.
 * Conventional-status use of the signal colours per the 2026-08-18
 * ruling. */
/* Coming-soon reveal (Joshua 2026-08-18): clicking an unavailable test dims
 * EVERY unavailable option in that container for the toast's duration, so
 * the moment of "no" also shows where "yes" lives. Opacity, not display —
 * the layout must not shift under the cursor. Cleared by timeout in
 * comingSoonMoment(); the transition covers both directions. */
.is-unavail-dim {
  opacity: 0.3 !important;
  filter: saturate(0.5);
  transition: opacity 0.3s var(--ease-out), filter 0.3s var(--ease-out);
}

/* Alert toast (Joshua 2026-08-18: coming-soon "more in your face"). TOP of
 * the screen, exclamation icon, caution-yellow edge — conventional-status
 * use of the signal colours per the 2026-08-18 ruling. Placement is part of
 * the meaning: error/success confirm something you did and sit quietly at
 * the bottom; alert contradicts what you just tried, so it meets your eye
 * line instead. */
.stl-toast--top {
  bottom: auto;
  top: max(var(--s-6), env(safe-area-inset-top));
  transform: translate(-50%, -12px);
  display: inline-flex;
  align-items: center;
  gap: 9px;
}
.stl-toast--top.is-visible { transform: translate(-50%, 0); }
.stl-toast__icon {
  width: 16px; height: 16px; flex: 0 0 auto;
  color: var(--stl-yellow);
}
.stl-toast[data-tone="alert"] {
  border-color: rgba(var(--stl-yellow-rgb), 0.55);
  box-shadow: inset 3px 0 0 var(--stl-yellow), 0 12px 36px -10px rgba(0, 0, 0, 0.55);
}
:root[data-theme="light"] .stl-toast[data-tone="alert"] {
  box-shadow: inset 3px 0 0 var(--stl-yellow), 0 12px 36px -10px rgba(15, 23, 42, 0.28);
}

.stl-toast[data-tone="error"] {
  border-color: rgba(var(--stl-red-rgb), 0.55);
  box-shadow: inset 3px 0 0 var(--stl-red), 0 12px 36px -10px rgba(0, 0, 0, 0.55);
}
.stl-toast[data-tone="success"] {
  border-color: rgba(var(--stl-green-rgb), 0.55);
  box-shadow: inset 3px 0 0 var(--stl-green), 0 12px 36px -10px rgba(0, 0, 0, 0.55);
}
:root[data-theme="light"] .stl-toast[data-tone="error"] {
  box-shadow: inset 3px 0 0 var(--stl-red), 0 12px 36px -10px rgba(15, 23, 42, 0.28);
}
:root[data-theme="light"] .stl-toast[data-tone="success"] {
  box-shadow: inset 3px 0 0 var(--stl-green), 0 12px 36px -10px rgba(15, 23, 42, 0.28);
}

/* ============== responsive =========================================
 * Strategy: the layout is fluid (clamp-based) by default, so most of
 * the mobile work is done by the cards/inputs/typography downscaling
 * naturally. These breakpoints fix the things fluid sizing can't —
 * grid topology (columns → rows), tap-target minimums, hover-on-
 * touch stickiness, and the iOS "input zooms when font-size < 16px"
 * footgun.
 *
 * Three breakpoints:
 *   ≤768px tablet       — small spacing tweaks, header shrinks
 *   ≤560px phone        — buttons full-width, signal stacks 1-col,
 *                         tap targets ≥44px, button rows collapse
 *   ≤400px narrow phone — bento becomes 1-col so labels never wrap
 *                         awkwardly; numerals scale down a notch
 *
 * Plus: hover-on-touch override, and a short-screen (landscape phone)
 * vertical compaction.
 *
 * In-place mobile overrides for charts/tables/choice-grid live next
 * to their subsystem rules above so subsystems stay self-contained;
 * everything cross-cutting lives here.
 * =================================================================== */

/* Hover styles only fire on hover-capable pointers. On phones / tablets
 * the same selectors leave hover styling stuck after a tap, which reads
 * as a bug. This media query overrides hover to the resting state on
 * touch devices. */
@media (hover: none) {
  .stl-btn:hover,
  .dc-btn:hover {
    background: var(--stl-surface);
    border-color: var(--stl-hairline-hi);
    filter: none;
  }
  .stl-btn--ghost:hover {
    background: transparent;
    color: var(--stl-text-muted);
  }
  /* Primary buttons: the .stl-btn:hover rule above strips the button
   * back to var(--stl-surface) (the muted base). On touch devices
   * iOS/Android persist :hover after a tap, so the primary button would
   * lose its rainbow border and sit there as a plain grey slab. (Bug
   * surfaced 2026-05-07 on the review-screen Next button.) Restore the
   * resting rainbow outline so the primary button always reads as the
   * primary CTA, even when the browser thinks it's "hovered".
   * Updated 2026-08-05: this used to restore the old FILLED lavender
   * gradient; --primary is border-only now, so it restores that. */
  .stl-btn--primary:hover,
  .stl-btn--rainbow-outline:hover {
    background:
      linear-gradient(var(--stl-rainbow-fill), var(--stl-rainbow-fill)) padding-box,
      var(--stl-rainbow) border-box;
    border-color: transparent;
    color: var(--stl-text);
  }
  .stl-signal__btn:hover {
    transform: none;
    background: var(--stl-surface);
  }
  .stl-signal__btn--green:hover  { border-color: var(--stl-hairline-hi); }
  .stl-signal__btn--yellow:hover { border-color: var(--stl-hairline-hi); }
  .stl-signal__btn--red:hover    { border-color: var(--stl-hairline-hi); }
  .stl-choice:hover,
  .stl-choice--chart:hover {
    background: var(--stl-surface);
    border-color: var(--stl-hairline);
  }
  .stl-input:hover,
  .stl-quiz__gridin-input:hover { border-color: var(--stl-hairline-hi); }
}

/* ---------- toggles go icon-only earlier (≤ 1280px) ----------
 * "Dark mode" / "Modern view" are two-word labels that wrap before the
 * rest of the header runs out of room, so collapse just these utility
 * toggles to their state-swapping icon ahead of the 1024px cleanup.
 * Icons still swap (moon/sun, grid/rows); aria-labels keep them named. */
@media (max-width: 1280px) {
  .stl-toggle__lbl { display: none; }
  .stl-theme-toggle { gap: 0; padding: 6px; }
}

/* ---------- below desktop (≤ 1024px): de-clutter the header ----------
 * An admin/teacher carries up to 4 nav tabs + 2 toggles + the New-test
 * CTA + the user menu, which crowds (and wraps) below desktop. Drop the
 * decorative nav-link icon chips — the label is the button's accessible
 * name, so nothing is lost — and collapse the user-menu trigger from the
 * full email down to a single user glyph. */
/* ── HEADER TIER A — drop nav icons, collapse the email to a glyph ────
 * TWO blocks, identical declarations, different triggers. The header's
 * content width is not a function of the viewport: a signed-in TEACHER
 * carries a 5th nav tab plus the email pill, ~300px more than the
 * signed-out header these breakpoints were tuned against. So the same
 * 1024px trigger that leaves comfortable slack signed-out arrives ~300px
 * too late for Joshua, and the band between is where the tabs folded
 * (reported at ~1071px, 2026-08-11).
 *
 * No measurement needed: the content has a small number of discrete
 * widths (the email is already capped at 22ch) and the teacher tab is
 * already toggled via the `hidden` attribute, so :has() can read the
 * real composition. Same idiom the test-tint gating uses.
 *
 * The two blocks MUST declare the same properties —
 * tests/header-tiers.test.js fails if they drift. */
@media (max-width: 1024px) {
  .stl-header__nav-icon { display: none; }
  .stl-header__user-email { display: none; }
  .stl-header__usermenu-chev { display: none; }
  .stl-header__user-icon { display: inline-flex; }
  .stl-header__usermenu-trigger { padding: 6px; }
}
@media (max-width: 1320px) {
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__nav-icon { display: none; }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__user-email { display: none; }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__usermenu-chev { display: none; }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__user-icon { display: inline-flex; }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__usermenu-trigger { padding: 6px; }
}

/* ---------- tablet (≤ 768px) ----------
 * HEADER TIER B — tighten the bar and swap the nav labels for their
 * short forms ("How it works" -> "How"). Paired with a teacher-offset
 * block exactly like tier A above; the two must stay in sync and
 * tests/header-tiers.test.js enforces it. .stl-screen's gap change is
 * viewport-only and deliberately NOT duplicated — it is page rhythm,
 * not header composition. */
@media (max-width: 768px) {
  .stl-header {
    height: calc(54px + env(safe-area-inset-top));
    gap: var(--s-3);
  }
  .stl-header__name { font-size: 13.5px; }
  .stl-header__left { gap: var(--s-3); }
  .stl-header__nav { gap: 0; }
  .stl-header__nav-link { padding: 0 10px; font-size: 13px; }
  .stl-header__nav-label { display: none; }
  .stl-header__nav-label-sm { display: inline; }
  .stl-screen { gap: var(--s-5); }
}
@media (max-width: 1040px) {
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header {
    height: calc(54px + env(safe-area-inset-top));
    gap: var(--s-3);
  }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__name { font-size: 13.5px; }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__left { gap: var(--s-3); }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__nav { gap: 0; }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__nav-link { padding: 0 10px; font-size: 13px; }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__nav-label { display: none; }
  :root:has(.stl-header__nav-link[data-nav="teacher"]:not([hidden])) .stl-header__nav-label-sm { display: inline; }
}

/* ---------- phone (≤ 560px) ---------- */
@media (max-width: 560px) {
  /* Header — keep timer + actions visible, drop the eyebrow if it
   * existed (already gone), and tighten gap so a long Restart label
   * doesn't push the timer offscreen. */
  .stl-header__eyebrow { display: none; }
  .stl-header__actions { gap: 6px; }
  .stl-header__name { font-size: 13px; }

  /* Signal trio stacks vertically — reach is what matters most when
   * you're answering the question, and the green/yellow/red labels
   * read more clearly at full-width than crammed three-up. */
  .stl-signal { grid-template-columns: 1fr; gap: 10px; }
  .stl-signal__btn {
    min-height: 64px;            /* still huge tap target, less vertical real estate */
    padding: var(--s-3) var(--s-4);
    grid-template-columns: auto auto 1fr;
    grid-template-areas: "chip lbl sub";
    gap: var(--s-3);
    align-items: center;
    align-content: center;
    justify-items: start;
  }
  .stl-signal__chip { grid-area: chip; }
  .stl-signal__lbl  { grid-area: lbl; font-size: 1rem; }
  .stl-signal__sub  {
    grid-area: sub;
    text-align: right;
    justify-self: end;
    color: var(--stl-text-subtle);
  }

  /* Quiz signal helper text — small all-caps mono is too small at
   * mobile reading distance, bump a touch and loosen letter-spacing. */
  .stl-quiz__signal-help { font-size: 11.5px; letter-spacing: 0.08em; }

  /* Bigger touch targets on the small footprint buttons in the header
   * and elsewhere. iOS HIG says 44pt; we hit that with 12px padding
   * + ~14px text + line-height. */
  .stl-btn,
  .dc-btn {
    padding: 11px 16px;
    font-size: 14px;
    min-height: 44px;
  }
  /* 12.5px not 13px — --primary carries a 1.5px border vs the base
   * 1px, so it needs half a pixel less padding to land on the same
   * 48px tap target as its neighbours. */
  .stl-btn--primary,
  .stl-btn--rainbow-outline { padding: 12.5px 18px; min-height: 48px; }
  .stl-btn--dev    { padding: 7px 11px; min-height: 0; }     /* dev stays compact */

  /* On narrow phones the share row (3 buttons) and take-another
   * row (2 buttons) collapse to single-column so labels never
   * truncate. Each button takes the full row width. */
  .stl-results__group-row > .stl-btn,
  .stl-results__group-row > .dc-btn {
    flex: 1 1 100%;
    width: 100%;
    min-width: 0;
  }

  /* Card padding tightens — already clamps, but enforce a floor */
  .stl-card { padding: var(--s-4); }
  .stl-card--hero { padding: var(--s-5) var(--s-4); }

  /* Score input + grid-in input — already ≥18px so no iOS zoom; but
   * widen tap target a touch */
  .stl-input,
  .stl-quiz__gridin-input { padding: 14px 14px; }

  /* Score-screen tier pills — they're informational, can shrink */
  .stl-tier { font-size: 10.5px; padding: 5px 10px; }
  .stl-score__tiers { gap: 6px; }

  /* Review choices: bump to 44px tap height in case the user is
   * tapping rather than scanning */
  .stl-review__choices li { padding: 13px var(--s-4); font-size: 0.94rem; }

  /* Review nav: split the row evenly */
  .stl-review__nav { gap: var(--s-2); }
  .stl-review__nav .stl-btn { flex: 1; }

  /* Stem / passage typography — tighter line-height feels less letter-
   * spacing-y on mobile narrow widths */
  .stl-quiz__stem { font-size: 1.1rem; line-height: 1.4; }
  .stl-quiz__passage { font-size: 0.92rem; padding: var(--s-2) var(--s-3); }

  /* Backdrop blur is expensive on mid-range Android. Drop the radius
   * by half — the bar still reads as glass. */
  .stl-header {
    backdrop-filter: saturate(180%) blur(8px);
    -webkit-backdrop-filter: saturate(180%) blur(8px);
  }

  /* Ambient glow — shrink so it doesn't bleed across the whole canvas
   * on small screens (it's a top-right halo, not a centered wash). */
  .stl-app::before {
    width: 480px;
    height: 480px;
    top: -180px;
    right: -120px;
  }
}

/* ---------- narrow phone (≤ 400px) ----------
 * iPhone SE 1st gen (320px) and Galaxy Fold cover-screen sit here.
 * The 2-col bento gets cramped at this width — labels like
 * "True mastery green + correct" wrap weirdly — so flatten to 1-col. */
@media (max-width: 400px) {
  .stl-results__grid { grid-template-columns: 1fr; }
  .stl-h1 { font-size: clamp(1.65rem, 7vw, 2rem); }
}

/* ---------- short screens (landscape phone) ----------
 * Squish vertical breathing room so the score / signal screens still
 * fit on screen without scrolling when a phone is rotated. */
@media (max-height: 560px) and (orientation: landscape) {
  .stl-header { height: calc(48px + env(safe-area-inset-top)); }
  .stl-main { padding-top: var(--s-5); padding-bottom: var(--s-5); }
  .stl-card--hero { padding: var(--s-4); }
  .stl-screen { gap: var(--s-3); }
  .stl-quiz__card { gap: var(--s-3); }
  .stl-h1 { font-size: clamp(1.5rem, 4vw, 2rem); }
  .stl-quiz__figure[data-kind="chart"] { height: 220px; }
  .stl-signal__btn { min-height: 56px; }
}

/* --- view transitions ------------------------------------------------
 * Native View Transitions API powers the screen morphs (score → quiz →
 * results → review → regen). The runtime snapshots before/after states
 * and animates between them. These keyframes give the swap a soft drift
 * so screens feel like *places* the user travels between, not panels
 * that flicker.
 *
 * The shared-element morph (active tier pill → difficulty pill) is set
 * up by giving both elements the same `view-transition-name`. Only one
 * is ever on screen at a time, which is what the API needs to morph.
 */
/* Hoist the header out of the root snapshot so it never participates
 * in screen transitions. Pairing the same name on old + new means the
 * View Transitions API morphs the header from itself to itself, and
 * since both snapshots are visually identical the morph is a no-op:
 * the navbar stays bolted to the top through every screen change.
 * Without this, the root snapshot's animation (even opacity-only)
 * carries the header along and reads as a navbar bounce/flicker. */
.stl-header { view-transition-name: stl-header; }
::view-transition-group(stl-header),
::view-transition-old(stl-header),
::view-transition-new(stl-header) {
  animation: none;
}

/* Root snapshot animation kept subtle — opacity-only fade for the
 * surrounding chrome that doesn't have its own view-transition-name. */
@keyframes stl-vt-old { to   { opacity: 0; } }
@keyframes stl-vt-new { from { opacity: 0; } }
::view-transition-old(root) {
  animation: stl-vt-old 180ms ease both;
}
::view-transition-new(root) {
  animation: stl-vt-new 220ms ease both;
}

/* shared-element morph: the active tier pill on the score screen
 * physically carries into the difficulty pill on the quiz screen */
.stl-tier.is-active { view-transition-name: stl-active-pill; }
#quiz-difficulty    { view-transition-name: stl-active-pill; }

/* the morph itself gets a slightly slower, springier curve so the
 * pill's flight reads as deliberate */
::view-transition-group(stl-active-pill) {
  animation-duration: 360ms;
  animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
}

/* --- card slide (iOS-style page advance) -----------------------------
 * Every screen's primary card (score, quiz, results, review, regen
 * — they all share .stl-card--hero) participates in a horizontal
 * slide motion. The View Transitions API hoists the named card out
 * of the root snapshot, so its animation runs separately from the
 * surrounding chrome (header, signal buttons, etc.) which still uses
 * the gentler root drift-fade defined above.
 *
 * Direction:
 *   default  (forward)        → old exits left,  new enters from right
 *   html.stl-going-back        → old exits right, new enters from left
 *
 * The 110% overshoot makes each card fully clear the viewport before
 * the other arrives — this is what gives it the iOS-page feel rather
 * than a crossfade. Both directions also fade the opacity to 0 at the
 * extremes so a card visibly dissolves as it leaves and resolves as
 * it arrives, instead of just tracking off the side.
 */
/* Only the CURRENTLY VISIBLE hero card gets the view-transition-name,
 * so the View Transitions API doesn't have to disambiguate between
 * multiple cards in the DOM that all share the class. With both
 * picker and score cards in the document, having the name on every
 * .stl-card--hero made the API pair the snapshots inconsistently —
 * picker (declared first in DOM) sometimes won as the "old" element
 * regardless of which screen the user was actually leaving, breaking
 * the back-direction animation. Scoping to the non-hidden screen
 * means at snapshot time, only one card carries the name. */
.stl-screen:not([hidden]) .stl-card--hero {
  view-transition-name: stl-card-hero;
}

@keyframes stl-card-out-left  { to   { transform: translateX(-110%); opacity: 0; } }
@keyframes stl-card-in-right  { from { transform: translateX( 110%); opacity: 0; } }
@keyframes stl-card-out-right { to   { transform: translateX( 110%); opacity: 0; } }
@keyframes stl-card-in-left   { from { transform: translateX(-110%); opacity: 0; } }

/* Pure crossfade — used by withTransition when the direction is 'fade'
 * (admin screen entry/exit). No translation, just opacity, so the
 * shape change between the page-wide admin canvas and the card-shaped
 * other screens doesn't read as a horizontal expand. */
@keyframes stl-vt-fade-out { to   { opacity: 0; } }
@keyframes stl-vt-fade-in  { from { opacity: 0; } }

/* Animation NAME comes from a CSS variable set on <html> by
 * withTransition() in app.js. Forward → out-left + in-right;
 * Back → out-right + in-left. Variables (rather than competing
 * specificity rules) sidestep the cascade ambiguity Chrome's view-
 * transition pseudo-element implementation occasionally exhibits.
 * Fallback values cover the case where the variable hasn't been set
 * (initial paint / no transition in flight). */
::view-transition-old(stl-card-hero) {
  /* easeOutCubic — pure deceleration into the off-screen edge with
   * no rubber-band rebound. The previous cubic-bezier(0.32, 0.72, 0, 1)
   * had a snappy "settle" that read as a bounce when paired with the
   * incoming card on tab clicks. */
  animation: var(--vt-card-out, stl-card-out-left) 320ms cubic-bezier(0.33, 1, 0.68, 1) both;
}
::view-transition-new(stl-card-hero) {
  animation: var(--vt-card-in, stl-card-in-right) 320ms cubic-bezier(0.33, 1, 0.68, 1) both;
}

/* First paint: there's no prior state for the View Transition system
 * to morph from, so the score card slides in from the right via a
 * regular CSS animation. The mounting class is set on <body> in app.js
 * and removed once this animation completes, so it only runs on
 * initial load — subsequent visits to the score screen (regen → score,
 * restart → score) go through the View Transition path with the back-
 * direction class instead.
 */
/* (Removed: the .stl-app--mounting first-paint slide-in. The score
 * card now appears instantly on initial load. View Transitions still
 * power navigation between screens.) */

/* --- micro-interactions --------------------------------------------- */

/* signal-button press feels haptic-like — a soft scale + shadow drop.
 * the existing :hover lift becomes a settle on press, before the
 * answer is recorded. */
.stl-signal__btn:active:not(:disabled) {
  transform: translateY(0) scale(0.985);
  background: var(--stl-surface-3);
}

/* choice press — a subtle inward press matching button feel */
.stl-choice:active {
  transform: scale(0.995);
}
/* (Stat hover styling lives further up the file under the per-variant
 * --stat-color block. There used to be duplicate .stl-stat:hover and
 * .stl-stat--featured:hover rules here that re-set border-color to
 * --stl-hairline-hi and --stl-accent respectively, which is what was
 * painting the lavender "weird purple border" on True Mastery on
 * hover. Removing them lets the per-variant rules win. */

/* --- print: trouble problems → PDF -----------------------------------
 * The live app is dark-themed and uses a single-card-at-a-time view —
 * neither of which works on paper. When the user clicks "Save trouble
 * problems," app.js builds a #stl-print-root with all the problems
 * inlined; this CSS hides everything else, flips to a light theme,
 * and adds page-break rules so questions don't split across pages.
 *
 * On screen, #stl-print-root is hidden by default so it doesn't show
 * up between print jobs. */
.stl-print-root { display: none; }

@media print {
  @page { margin: 18mm 16mm; }

  /* Force light theme — body's --stl-bg-deep would otherwise eat the
   * paper. !important needed because the .stl-app rules use specific
   * dark backgrounds that would otherwise cascade. */
  html, body {
    background: #fff !important;
    color: #000 !important;
  }

  /* Hide the entire live app while printing; show only the print root.
   * body > * doesn't work cleanly because the print root sits as a
   * sibling — we use direct visibility/display so the print preview
   * renders only the print content. */
  body > .stl-app { display: none !important; }
  /* The app root class lives ON <body> (not a child), so the rule above
   * matches nothing — hide every direct child except the print root.
   * (This also fixes the pre-existing trouble-problems export printing
   * the app chrome on page 1.) */
  body.stl-app:has(> .stl-print-root) > :not(.stl-print-root) { display: none !important; }
  body > .stl-print-root { display: block !important; }

  .stl-print-root {
    color: #000;
    background: #fff;
    font-family: Georgia, "Source Serif 4", "Iowan Old Style", serif;
    font-size: 11pt;
    line-height: 1.5;
    max-width: none;
  }

  /* Header: title + meta line (count, target, time, date). */
  .stl-print-head {
    border-bottom: 1px solid #999;
    padding-bottom: 10pt;
    margin-bottom: 14pt;
  }
  .stl-print-head h1 {
    font-family: "Geist", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
    font-size: 19pt;
    font-weight: 600;
    margin: 0 0 4pt;
    letter-spacing: -0.02em;
    color: #000;
  }
  .stl-print-head__meta {
    font-family: var(--stl-font-mono);
    font-size: 9pt;
    color: #555;
    margin: 0;
    letter-spacing: 0.04em;
  }

  /* Each problem: keep on one page if at all possible. */
  .stl-print-q {
    page-break-inside: avoid;
    break-inside: avoid;
    margin: 0 0 14pt;
    padding: 0 0 10pt;
    border-bottom: 1px solid #ddd;
  }
  .stl-print-q:last-child { border-bottom: 0; }

  .stl-print-q__head {
    font-family: var(--stl-font-mono);
    font-size: 9pt;
    color: #666;
    margin: 0 0 6pt;
    letter-spacing: 0.05em;
    text-transform: uppercase;
  }
  .stl-print-q__num {
    color: #000;
    font-weight: 600;
    margin-right: 8pt;
    font-family: "Geist", -apple-system, system-ui, sans-serif;
    font-size: 11pt;
    letter-spacing: 0;
    text-transform: none;
  }

  .stl-print-q__stem {
    margin: 0 0 8pt;
    font-size: 11pt;
    color: #000;
  }

  .stl-print-q__choices {
    list-style: none;
    margin: 0 0 8pt;
    padding: 0 0 0 14pt;
  }
  .stl-print-q__choice {
    margin: 0 0 3pt;
    padding: 0;
    color: #000;
  }
  .stl-print-q__letter {
    font-weight: 600;
    margin-right: 4pt;
    font-family: var(--stl-font-mono);
    font-size: 10pt;
  }
  .stl-print-q__choice.is-correct {
    color: #1a6b35;          /* dark green that prints crisply on paper */
    font-weight: 600;
  }
  .stl-print-q__choice.is-picked {
    color: #8b1f1f;          /* dark red */
    text-decoration: line-through;
  }
  .stl-print-q__mark {
    font-family: var(--stl-font-mono);
    font-size: 9pt;
    margin-left: 6pt;
    text-decoration: none;
  }

  .stl-print-q__gridin {
    margin: 0 0 6pt;
    font-size: 11pt;
    padding-left: 14pt;
  }

  .stl-print-q__expl {
    margin: 6pt 0 0;
    padding: 4pt 0 4pt 12pt;
    border-left: 2pt solid #aaa;
    font-style: italic;
    color: #333;
    font-size: 10pt;
  }

  /* Variable styling: italic, but no lavender on paper. */
  .stl-print-root .stl-quiz__var {
    font-style: italic;
    color: #000;
  }
  /* Math fractions in superscripts (the stacked p/q renderer) print as-is */
  .stl-print-root .stl-math-fracsup,
  .stl-print-root .stl-math-sup {
    color: #000;
  }

  /* Figures. Image figures invert back to light-theme since the source
   * screenshots are white-bg-black-text screenshots that we display
   * with an invert filter on screen. SVG figures use currentColor and
   * print directly. Tables inherit the print color scheme. */
  .stl-print-q__figure {
    margin: 4pt 0 8pt;
    text-align: left;
  }
  .stl-print-q__figure img {
    max-width: 80%;
    height: auto;
    /* Source screenshots are white-on-dark already — print them as is */
  }
  .stl-print-q__figure[data-kind="svg"] svg {
    max-width: 80%;
    height: auto;
    color: #000;
  }
  .stl-print-q__figure[data-kind="table"] table {
    border-collapse: collapse;
    margin: 4pt 0;
    font-size: 10pt;
    font-family: "Geist", system-ui, sans-serif;
  }
  .stl-print-q__figure[data-kind="table"] caption {
    text-align: left;
    font-weight: 600;
    margin-bottom: 4pt;
    font-size: 10pt;
  }
  .stl-print-q__figure[data-kind="table"] th,
  .stl-print-q__figure[data-kind="table"] td {
    border: 1px solid #999;
    padding: 4pt 8pt;
    text-align: right;
  }
  .stl-print-q__figure[data-kind="table"] th:first-child,
  .stl-print-q__figure[data-kind="table"] td:first-child {
    text-align: left;
  }
  .stl-print-q__figure[data-kind="table"] th {
    background: #eee !important;
    font-weight: 600;
  }
  .stl-print-q__figure--placeholder {
    font-family: var(--stl-font-mono);
    font-size: 9pt;
    color: #888;
    border: 1px dashed #bbb;
    padding: 8pt;
    text-align: center;
  }

  /* Worksheet builder print additions: answer key starts on its own
   * page; blanks/name line print crisply. */
  .stl-print-key { page-break-before: always; break-before: page; }
  .stl-print-key h2 { font-family: "Geist", -apple-system, system-ui, sans-serif; font-size: 14pt; color: #000; }
  .stl-print-key__grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4pt 14pt; }
  .stl-print-nameline { display: flex; gap: 22pt; flex-wrap: wrap; margin: 0 0 16pt; font-size: 11pt; }
  .stl-print-q__blank { display: inline-block; min-width: 140pt; border-bottom: 1pt solid #333; height: 1em; }
  .stl-print-q__blank--sm { min-width: 70pt; }
  .stl-print-q__sprline { padding-left: 14pt; font-size: 11pt; }
}

/* --- reduced motion ------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  /* explicitly suppress view-transition pseudo-elements too — the
   * universal selector above doesn't reach them in every engine */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
.stl-mobile-bar       { display: none; }

/* The splash screen exists only on mobile. Hard-hide it on desktop
 * regardless of the JS-managed [hidden] attr so it never paints
 * on a desktop viewport, even briefly during init. */
@media (min-width: 641px) {
  [data-screen="splash"] { display: none !important; }
}

@media (max-width: 640px) {
  /* hide the desktop header and let main fill the viewport */
  .stl-header { display: none; }
  .stl-main {
    max-width: none;
    margin: 0;
    padding: 0;
    /* No `min-height: 100dvh` here. main is body's flex-grow child, so
     * it already takes the full viewport minus the footer — asking for
     * a whole 100dvh on top of that just pushed the footer past the
     * fold on every single phone screen. */
  }
  /* iOS Safari background bleed */
  .stl-app::before { display: none; }

  /* every screen becomes a fullscreen app surface */
  .stl-screen {
    /* `100dvh` here meant "as tall as the viewport", but the screen sits
     * inside main, which sits above the footer — so a full 100dvh always
     * overshot by the footer's height. `100%` fills main instead, which
     * IS the leftover viewport now that body is a flex column. Same
     * fullscreen feel, no phantom scroll. */
    min-height: 100%;
    padding-top: max(env(safe-area-inset-top), var(--s-3));
    padding-right: max(env(safe-area-inset-right), var(--s-4));
    padding-bottom: max(env(safe-area-inset-bottom), var(--s-4));
    padding-left: max(env(safe-area-inset-left), var(--s-4));
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
  }

  /* hero cards lose card chrome — the screen is the card */
  .stl-card--hero {
    background: transparent;
    border: 0;
    box-shadow: none;
    padding: 0;
    border-radius: 0;
    /* let the content breathe vertically inside the screen flow */
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
  }

  /* ─── shared mobile bar (results / review / history) ─── */
  .stl-mobile-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    height: 44px;
    flex-shrink: 0;
  }
  .stl-mobile-bar__btn {
    appearance: none; -webkit-appearance: none;
    width: 40px; height: 40px;
    display: grid; place-items: center;
    background: transparent;
    color: var(--stl-text-muted);
    border: 1px solid var(--stl-hairline);
    border-radius: var(--r-pill);
    font: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color var(--dur-base) var(--ease-spring),
                border-color var(--dur-base) var(--ease-spring),
                background var(--dur-base) var(--ease-spring);
  }
  .stl-mobile-bar__btn:active {
    color: var(--stl-text);
    border-color: var(--stl-accent-line);
    background: var(--stl-accent-soft);
  }
  /* The author `display: grid` above wins over the UA `[hidden]
   * { display: none }` rule — author origin beats UA regardless of
   * specificity — so the hidden attribute alone would not collapse this
   * button. Same hardening .stl-quiz__tools[hidden] already carries.
   * Without it the How page's back arrow paints on the homepage even
   * though the JS correctly marked it hidden. */
  .stl-mobile-bar__btn[hidden] { display: none !important; }
  .stl-mobile-bar__title {
    flex: 1;
    text-align: center;
    font-family: var(--stl-font-mono);
    font-size: 12px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--stl-text-muted);
  }
  /* The How page is the signed-out homepage, so its bar wears the
   * wordmark instead of a screen title (Josh 2026-08-11: "technically
   * it's the homepage, so let's just have a logo"). Left-aligned rather
   * than centered — a centered logo reads as a title, a left-aligned one
   * reads as the top of an app. */
  .stl-mobile-bar__brand {
    flex: 1;
    display: flex;
    align-items: center;
    min-width: 0;
  }
  .stl-mobile-bar__brand .stl-brand__svg { height: 22px; }
  /* Nothing on the leading edge when the back button is hidden, so drop
   * the trailing spacer's reserved width too and let the mark sit flush
   * with the page gutter. */
  .stl-mobile-bar--home .stl-mobile-bar__accessory { display: none; }
  .stl-mobile-bar__accessory {
    min-width: 40px;
    text-align: right;
    font-family: var(--stl-font-mono);
    font-size: 13px;
    color: var(--stl-text-muted);
    font-variant-numeric: tabular-nums;
  }

  /* ============================================================
   * SPLASH SCREEN (mobile only)
   *
   * Welcome screen. Larger wordmark + tagline up top, primary
   * "Get started" CTA in the thumb zone with secondary actions
   * (History / Sign in / Admin) below it.
   * ============================================================ */
  [data-screen="splash"] {
    justify-content: stretch;
    gap: 0;
  }
  .stl-splash {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    max-width: 420px;
    margin: 0 auto;
    text-align: center;
    padding: var(--s-6) 0 var(--s-3);
  }
  .stl-splash__hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(14px, 3.5vw, 22px);
    margin-top: 10vh;
  }
  /* Mark scales with viewport width — bigger on roomy phones (Pro Max,
   * Android XL) without overflowing on narrow ones (iPhone SE). */
  .stl-splash__mark-svg {
    display: block;
    height: clamp(44px, 12vw, 64px);
    width: auto;
    color: var(--stl-text);
  }
  .stl-splash__tagline {
    margin: 0;
    max-width: var(--measure-meta);
    font-size: clamp(15px, 4.2vw, 19px);
    line-height: 1.4;
    color: var(--stl-text-muted);
    letter-spacing: -0.005em;
  }
  .stl-splash__actions {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    align-items: center;
  }
  .stl-splash__cta {
    width: 100%;
    padding: 16px;
    font-size: 16px;
    border-radius: var(--r-md);
  }
  .stl-splash__secondary {
    display: flex;
    gap: var(--s-2);
    justify-content: center;
    flex-wrap: wrap;
  }
  .stl-splash__secondary .stl-btn {
    padding: 8px 14px;
    font-size: 13px;
  }

  /* ============================================================
   * SCORE SCREEN (mobile)
   *
   * After "Get started" the user lands here. Slim back-to-splash
   * bar at top, hero centered vertically.
   * ============================================================ */
  [data-screen="score"] {
    justify-content: flex-start;
    gap: 0;
  }
  /* Hero card fills the rest of the screen, content centered. */
  [data-screen="score"] .stl-card--hero {
    padding: 0;
    flex: 1;
    justify-content: center;
    gap: var(--s-3);
  }
  [data-screen="score"] .stl-eyebrow { display: none; }
  /* Center the block as well as the text. Desktop sets max-width: 18ch
   * for the H1 — without auto margins the constrained block sits flush
   * to the left edge of its parent, so `text-align: center` only
   * centers within that 18ch box, not on the screen. `margin: 0 auto`
   * centers the box; combined with `text-align: center` everything
   * lines up to the viewport's center axis. */
  [data-screen="score"] .stl-h1 {
    text-align: center;
    font-size: clamp(26px, 7vw, 32px);
    line-height: 1.15;
    margin: 0 auto;
    max-width: 22ch;
  }
  [data-screen="score"] .stl-sub {
    text-align: center;
    margin: 0 auto;
    max-width: 34ch;
    font-size: 14.5px;
  }
  [data-screen="score"] .stl-score__form {
    margin-top: var(--s-3);
    gap: var(--s-4);
  }
  [data-screen="score"] .stl-score__tiers {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
  }
  /* Idle chrome for tier buttons on mobile: visible border, subtle
   * surface fill, readable text. Desktop's hover-only chrome
   * doesn't survive on touch — nothing to hover with. */
  [data-screen="score"] .stl-tier {
    padding: 13px 10px;
    text-align: center;
    font-size: 12px;
    line-height: 1.3;
    border-color: var(--stl-hairline);
    background: var(--stl-surface);
    color: var(--stl-text);
  }
  [data-screen="score"] .stl-tier.is-active {
    border-color: var(--stl-accent-line);
    background: var(--stl-accent-soft);
  }
  /* Tailored opts out of the accent: see the block near .stl-tier.is-active.
   * Needs the same [data-screen] prefix to reach (0,3,0), or the rule above
   * wins and the tile turns green beside the green Easy pill. */
  /* The gradient treatment applies here too (Joshua 2026-08-17). This rule
   * exists because the mobile pills need their own idle/active chrome, and at
   * (0,3,0) it outranks the desktop `.stl-tier--tailored.is-active` — so
   * without repeating the gradient here, selecting Tailored on a phone fell
   * back to a plain muted border. Caught by rendering at 430px: the harness
   * reported "gradient layers on tailored: 0" while desktop reported 2.
   *
   * Same opaque-fill requirement as the desktop rule: --stl-surface is
   * rgba(255,255,255,0.035), which would let the full-saturation border
   * gradient flood the pill. */
  [data-screen="score"] .stl-tier--tailored.is-active {
    border-color: transparent;
    background:
      linear-gradient(var(--stl-bg), var(--stl-bg)) padding-box,
      var(--stl-rainbow) border-box;
  }
  [data-screen="score"] .stl-score__form .stl-btn--pill-rainbow {
    /* full-width hero CTA on phones — comfy thumb height and a
     * larger font so the rainbow-pill button reads as the page's
     * primary action. Keeps the pill radius from --pill-rainbow. */
    width: 100%;
    padding: 16px;
    font-size: 16px;
  }

  /* ============================================================
   * QUIZ SCREEN → FOCUS MODE (Slim Dock, Josh 2026-07-30)
   *
   * The desktop quiz header comes BACK on phones — but only for the
   * two things the mobile bar doesn't already carry: the signal tick
   * strip (the product's signature, previously desktop-only) and the
   * difficulty/topic pills. Counter, timer, and tools are duplicates
   * of the mobile bar and stay hidden. column-reverse puts the ticks
   * directly under the bar with the pills below, matching the
   * approved mock.
   * ============================================================ */
  [data-screen="quiz"] .stl-quiz__top {
    display: flex;
    flex-direction: column-reverse;
    gap: 10px;
    margin-bottom: 2px;
  }
  [data-screen="quiz"] .stl-quiz__counter,
  [data-screen="quiz"] #quiz-test-badge,
  [data-screen="quiz"] #quiz-timer,
  [data-screen="quiz"] #quiz-tools { display: none; }
  [data-screen="quiz"] .stl-quiz__meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
  }
  [data-screen="quiz"] .stl-quiz__meta .stl-pill {
    font-size: 9px;
    padding: 4px 9px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }
  [data-screen="quiz"] .stl-card--hero {
    /* let the question card breathe; signal sticks to bottom outside it */
    background: transparent;
    border: 0;
    box-shadow: none;
    padding: 0;
  }
  [data-screen="quiz"] .stl-quiz__stem {
    font-size: 18px;
    line-height: 1.4;
  }
  /* sticky signal at the bottom of the viewport, in the thumb zone.
   * Pulled out of the screen padding so it goes edge-to-edge with a
   * top divider; bottom padding respects the home-indicator inset. */
  [data-screen="quiz"] .stl-quiz__signal {
    position: sticky;
    bottom: 0;
    margin-left:  calc(-1 * max(env(safe-area-inset-left),  var(--s-4)));
    margin-right: calc(-1 * max(env(safe-area-inset-right), var(--s-4)));
    margin-bottom: calc(-1 * max(env(safe-area-inset-bottom), var(--s-4)));
    padding-left:  max(env(safe-area-inset-left),  var(--s-4));
    padding-right: max(env(safe-area-inset-right), var(--s-4));
    padding-top: var(--s-3);
    padding-bottom: max(env(safe-area-inset-bottom), var(--s-3));
    background: var(--stl-bg);
    border-top: 1px solid var(--stl-hairline);
    z-index: 10;
  }
  [data-screen="quiz"] .stl-quiz__signal-help {
    font-size: 11px;
    margin-bottom: 8px;
    text-align: center;
  }
  /* Slim Dock trio: same three .stl-signal__btn elements and handlers,
   * laid out as one row of three columns (chip over label over sub)
   * instead of three stacked full-width cards. Overrides the ≤560px
   * stacked-row layout defined earlier in this sheet — the
   * [data-screen] prefix outweighs it, and the grid-area resets below
   * undo that block's chip/lbl/sub area assignments. Dock height drops
   * from ~300px to ~150px. */
  [data-screen="quiz"] .stl-signal {
    grid-template-columns: repeat(3, 1fr);
    gap: 7px;
  }
  [data-screen="quiz"] .stl-signal__btn {
    min-height: 0;
    padding: 10px 6px 9px;
    display: grid;
    grid-template-columns: none;
    grid-template-areas: none;
    justify-items: center;
    align-content: center;
    gap: 4px;
    text-align: center;
    border-radius: 14px;
  }
  [data-screen="quiz"] .stl-signal__chip,
  [data-screen="quiz"] .stl-signal__lbl,
  [data-screen="quiz"] .stl-signal__sub {
    grid-area: auto;
    justify-self: center;
    text-align: center;
  }
  [data-screen="quiz"] .stl-signal__chip { width: 11px; height: 11px; }
  [data-screen="quiz"] .stl-signal__lbl { font-size: 13px; }
  [data-screen="quiz"] .stl-signal__sub { font-size: 9px; line-height: 1.2; }
  /* The step nav is the SAME control on mobile — back · skip/next in one
   * centred row — just tighter. It is the only back affordance on the
   * screen now; the mobile bar's left slot is empty by design (see
   * index.html). Targets stay ≥44px tall via the padding below. */
  [data-screen="quiz"] .stl-stepnav {
    gap: 10px;
    margin-top: 6px;
  }
  /* min-height, not padding. Padding of 11px looked like it would clear
   * 44px and did not — the rendered target measured 36px, because the
   * line box is only ~14px tall. State the number you actually care
   * about instead of a proxy for it. */
  [data-screen="quiz"] .stl-stepnav__btn {
    min-height: 44px;
    padding: 6px;
    font-size: 12px;
    gap: 5px;
  }
  [data-screen="quiz"] .stl-stepnav__rule { height: 13px; }

  /* ============================================================
   * RESULTS / REVIEW / HISTORY → list-style mobile
   * ============================================================ */
  [data-screen="results"] .stl-card--hero,
  [data-screen="review"]  .stl-card--hero,
  [data-screen="history"] .stl-card--hero,
  [data-screen="regen"]   .stl-card--hero {
    padding: 0;
  }
  /* hide the desktop quiz-top header on review (mobile bar replaces it) */
  [data-screen="review"] .stl-quiz__top { display: none; }
  /* tighten the stat grid on results for narrow screens */
  [data-screen="results"] .stl-results__grid {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--s-3);
  }
  /* sticky bottom action bar on results (primary share button) */
  [data-screen="results"] .stl-results__actions {
    margin-top: auto;
  }

  /* slimmer admin top — keeps the mobile bar pattern consistent */
  [data-screen="admin"] .stl-admin__page-head {
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-3);
  }
}

/* Phones smaller than ~380px: even tighter spacing for old iPhone SE etc. */
@media (max-width: 380px) {
  [data-screen="score"] .stl-h1 { font-size: 24px; }
  [data-screen="score"] .stl-tier { font-size: 11.5px; padding: 12px 8px; }
}

/* ============================================================
   Teacher shell (teacher mode — Phase 2). Same design language as
   the student app; a tabbed sub-nav (Students / Tests / Library /
   Insights / My practice) sits under the header. M1 panels are
   empty states. Role-gated to teacher/admin in app.js.
   ============================================================ */
.stl-teacher {
  /* No max-width here (2026-08-06). The 2026-07-26 "one canonical width"
   * fix landed twice at two different numbers: .stl-main:has(teacher) said
   * 1080 and this rule said 1000, and since this one is nested it silently
   * won — so 1080 was dead except in the ~1032-1112px band where the outer
   * calc() clamp is active and the two disagreed. Page width is now set in
   * exactly one place, the .stl-main shell rule. Inner blocks may cap
   * measure in ch, never shell in px. */
  width: 100%;
  margin: 0 auto;
  display: grid;
  gap: var(--s-4);
}
.stl-teacher__head { display: grid; gap: 4px; }
/* The retired .stl-teacher__tab* strip (~60 lines) was deleted 2026-08-18
 * (FIXLIST P3). teacher-shell.test.js asserts the markup stays gone; the
 * live tab bar is .stl-tabbar below. */
.stl-teacher__panel[hidden] { display: none; }
/* ====================================================================
 * .stl-tabbar — the shared tab bar
 * ====================================================================
 * A general N-pane view switcher, first used by "Manage students &
 * tests" (Students / Tests / Worksheets, Josh 2026-08-11).
 *
 * Deliberately NOT .stl-teacher__tab: that name belongs to the retired
 * teacher tab strip, and teacher-shell.test.js asserts it stays out of
 * the HTML. Deliberately not .stl-admin__tab either, even though the
 * look is the same — admin's rules are entangled with admin pane and
 * KPI selectors, and a teacher panel borrowing them would inherit
 * whatever admin does next. This is the same VISUAL language (muted
 * label, gradient underline on the active tab, mono count pill) as its
 * own component, so admin can migrate onto it later without either
 * side dragging the other around.
 * ================================================================== */
.stl-tabbar {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  border-bottom: 1px solid var(--stl-hairline);
  margin-bottom: var(--s-4);
}
.stl-tabbar__tab {
  appearance: none; -webkit-appearance: none;
  background: transparent;
  border: 0;
  /* The underline indicator is a pseudo pinned to the row's bottom
   * hairline, faded in by opacity rather than swapped by color, so the
   * gradient never re-rasterizes mid-transition. */
  position: relative;
  padding: 12px 4px;
  margin-bottom: -1px;           /* overlap the parent border */
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--stl-text-muted);
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-spring);
}
.stl-tabbar__tab + .stl-tabbar__tab { margin-left: var(--s-2); }
.stl-tabbar__tab::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 2px;
  background: var(--stl-rainbow);
  opacity: 0;
  transition: opacity var(--dur-base) ease;
  pointer-events: none;
}
.stl-tabbar__tab:hover { color: var(--stl-text); }
.stl-tabbar__tab:hover::after { opacity: 0.35; }
.stl-tabbar__tab[aria-selected="true"] { color: var(--stl-text); }
.stl-tabbar__tab[aria-selected="true"]::after { opacity: 1; }
.stl-tabbar__tab:focus-visible {
  outline: 2px solid rgba(var(--stl-rainbow-mid-rgb), 0.9);
  outline-offset: 2px;
  border-radius: 2px;
}
.stl-tabbar__count {
  font-family: var(--stl-font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--stl-text-faint);
  padding: 2px 8px;
  border-radius: var(--r-pill);
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  /* The count arrives after the pane's fetch. Without a floor the tab
   * label jumps sideways the moment it lands. */
  min-width: 28px;
  text-align: center;
}
/* Active tab's count — gradient border via the padding-box/border-box
 * trick, same as the admin tabs and the "All" history chip. */
.stl-tabbar__tab[aria-selected="true"] .stl-tabbar__count {
  border: 1px solid transparent;
  background:
    linear-gradient(var(--stl-bg-2), var(--stl-bg-2)) padding-box,
    var(--stl-rainbow) border-box;
  color: var(--stl-text);
}

/* ---- panes ---- */
.stl-tabbar__pane[hidden] { display: none; }
.stl-tabbar__pane {
  display: grid;
  gap: var(--s-5);
  position: relative;            /* anchors the veil */
}
/* Loading veil, same primitive as .stl-admin__pane-veil: covers the
 * pane while its fetch is in flight so an empty roster never paints as
 * if it were the answer. Raised on a delay by the JS so a warm tab
 * never flashes it. */
.stl-tabbar__veil {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: grid;
  place-items: center;
  /* --stl-surface is a translucent wash, so layer it over an opaque
   * base: half-painted rows underneath must not read through, but a
   * flat --stl-bg slab lands as a hole punched in the page. */
  background:
    linear-gradient(var(--stl-surface), var(--stl-surface)),
    var(--stl-bg);
  border-radius: var(--r-lg);
}
.stl-tabbar__veil[hidden] { display: none; }
/* Reserve height only while the veil is up, so an empty pane still has
 * something for it to cover and the veil never spills onto the footer. */
.stl-tabbar__pane[aria-busy="true"] { min-height: 220px; }

.stl-teacher__empty {
  display: grid;
  gap: var(--s-3);
  justify-items: start;
  text-align: left;
  padding: clamp(28px, 6vw, 56px) clamp(16px, 4vw, 32px);
  border: 1px dashed var(--stl-hairline);
  border-radius: var(--r-lg);
}
.stl-teacher__empty .stl-h2 { margin: 0; }
.stl-teacher__empty .stl-sub { max-width: 52ch; }
.stl-teacher__empty .stl-btn { margin-top: 4px; }
@media (max-width: 640px) {
}

/* ===== Teacher test composer (Tests tab) ===== */
.stl-composer {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: var(--s-4);
  align-items: start;
}
/* Both composer columns read as admin-style panels (surface + hairline)
 * so the Tests tab matches the carded admin aesthetic instead of fields
 * floating on the bare page. See .stl-admin__panel. */
.stl-composer__form {
  display: grid;
  gap: var(--s-3);
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  padding: var(--s-4);
}
.stl-composer__chips { display: flex; flex-wrap: wrap; gap: 6px; }
.stl-chip {
  appearance: none;
  border: 1px solid var(--stl-hairline);
  background: transparent;
  color: var(--stl-text);
  border-radius: var(--r-pill);
  padding: 6px 12px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.stl-chip:hover { background: var(--stl-accent-soft); }
.stl-chip.is-active {
  border-color: var(--stl-accent);
  background: var(--stl-accent-soft);
  color: var(--stl-text);
}
/* Test-type chips wear their test's own tint when selected (Josh
 * 2026-07-26): SAT purple, PSAT blue — set inline as --chip-tint. */
.stl-chip.is-active[style*="--chip-tint"] {
  border-color: rgb(var(--chip-tint));
  background: rgba(var(--chip-tint), 0.16);
}
.stl-composer__topics-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.stl-composer__count .stl-input { max-width: 140px; }
.stl-composer__preview {
  display: grid;
  gap: var(--s-3);
  padding: var(--s-4);
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  /* Clears the 60px sticky header the same way .stl-wk__side does. This
   * rule read `top: 16px` for a long time and looked fine only because
   * sticky was globally dead (see the .stl-app overflow note); the
   * moment it started latching, 16px parked the panel under the bar. */
  position: sticky;
  top: calc(84px + env(safe-area-inset-top));
  max-height: calc(100dvh - 84px - env(safe-area-inset-top) - 20px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
@media (max-width: 900px) {
  .stl-composer__preview {
    position: static;
    max-height: none;
    overflow-y: visible;
  }
}
.stl-composer__total { font-size: 15px; }
.stl-composer__total strong { font-size: 28px; font-weight: 700; }
.stl-composer__dist { display: grid; gap: 6px; }
.stl-composer__bar { display: grid; grid-template-columns: 56px 1fr 28px; align-items: center; gap: 8px; font-size: 12px; }
.stl-composer__bar-label { color: var(--stl-text-muted); }
.stl-composer__bar-track { height: 8px; border-radius: 4px; background: var(--stl-accent-soft); overflow: hidden; }
.stl-composer__bar-fill { display: block; height: 100%; background: var(--stl-accent); border-radius: 4px; transition: width 160ms ease; }
/* Tier-colored fills (Josh 2026-08-10) — same ramp as the tier pills:
 * easy green, medium yellow, hard red, insane blue. */
.stl-composer__bar-fill[data-tier="easy"]   { background: var(--stl-green); }
.stl-composer__bar-fill[data-tier="medium"] { background: var(--stl-yellow); }
.stl-composer__bar-fill[data-tier="hard"]   { background: var(--stl-red); }
.stl-composer__bar-fill[data-tier="insane"] { background: var(--stl-blue); }
.stl-composer__bar-n { text-align: right; color: var(--stl-text); font-variant-numeric: tabular-nums; }
.stl-composer__result { display: grid; gap: 6px; }
.stl-composer__linkrow { display: flex; gap: 8px; }
.stl-composer__linkrow .stl-input { flex: 1; min-width: 0; font-size: 13px; }
@media (max-width: 760px) {
  .stl-composer { grid-template-columns: 1fr; }
  .stl-composer__preview { position: static; }
}

/* ===== Teacher-assigned test landing (/t/<token>) ===== */
.stl-take {
  max-width: 620px;
  margin: 0 auto;
  display: grid;
  gap: var(--s-4);
  justify-items: stretch;
  text-align: left;
  padding: clamp(28px, 5vw, 48px);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background: var(--stl-surface);
}
.stl-take__head { display: grid; gap: var(--s-3); }
.stl-take .stl-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--stl-text-muted);   /* colour only — format is the primitive's */
  margin: 0;
}
.stl-take .stl-h1 { margin: 0; }
.stl-take__title { font-size: clamp(2rem, 5vw, 3rem); line-height: 1.04; }
/* Details reuse the create-test section card; lay out as label/value rows. */
.stl-take__details { margin: 0; }
.stl-take__specs { display: grid; gap: 10px; }
.stl-take__spec { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.stl-take__spec-k { color: var(--stl-text-muted); font-size: 14px; }
.stl-take__spec-v { color: var(--stl-text); font-weight: 600; text-align: right; }
.stl-take__cta { display: grid; gap: 8px; width: 100%; margin-top: 4px; }

/* ===== Teacher: roster list + student profile + matrix ===== */
.stl-roster { display: grid; gap: 6px; }
.stl-roster__row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  padding: 12px 14px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  background: transparent;
  font: inherit;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.stl-roster__row:hover { background: var(--stl-accent-soft); border-color: var(--stl-accent); }
.stl-roster__name { font-weight: 600; color: var(--stl-text); }
.stl-roster__meta { color: var(--stl-text-muted); font-size: 13px; font-variant-numeric: tabular-nums; }
/* Traffic-light mastery dot */
.stl-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.stl-dot--green  { background: var(--stl-green); }
.stl-dot--yellow { background: var(--stl-yellow); }
.stl-dot--red    { background: var(--stl-red); }
.stl-dot--grey   { background: var(--stl-text-subtle); }
/* Profile */
.stl-profile__head { margin: 12px 0; }
.stl-profile__head .stl-h2 { margin: 0; }
.stl-profile__section { display: grid; gap: 10px; margin-top: var(--s-4); }
.stl-attempt-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.stl-attempt-row {
  display: flex; justify-content: space-between; gap: 12px; align-items: baseline;
  padding: 8px 12px; border: 1px solid var(--stl-hairline); border-radius: 10px;
}
.stl-attempt-row__name { font-weight: 500; }
.stl-attempt-row__meta { color: var(--stl-text-muted); font-size: 13px; font-variant-numeric: tabular-nums; }
/* Performance matrix (topic × difficulty) */
.stl-matrix-wrap { overflow-x: auto; }
.stl-matrix { border-collapse: separate; border-spacing: 3px; width: 100%; font-size: 13px; }
.stl-matrix th { font-weight: 600; color: var(--stl-text-muted); text-align: center; padding: 4px 8px; font-size: 12px; }
.stl-matrix th:first-child { text-align: left; }
.stl-matrix__topic { padding: 6px 10px; white-space: nowrap; color: var(--stl-text); }
.stl-matrix__cell {
  text-align: center; padding: 6px 10px; border-radius: 6px; min-width: 46px;
  font-variant-numeric: tabular-nums; color: var(--stl-text);
}
.stl-cell--green  { background: rgba(var(--stl-green-rgb),0.18); }
.stl-cell--yellow { background: rgba(var(--stl-yellow-rgb),0.20); }
.stl-cell--red    { background: rgba(var(--stl-red-rgb),0.16); }
.stl-cell--grey   { background: var(--stl-accent-soft); color: var(--stl-text-muted); }

/* ===== Teacher: your-tests list + results =====
 * Row anatomy: [name + meta stacked | status chip | chevron].
 * Name ellipsizes; meta is quiet tabular detail; the status chip
 * reads at a glance (green dot = live). */
.stl-mytests { margin-top: var(--s-5); display: grid; gap: 10px; }
.stl-mytests__list { display: grid; gap: 8px; }
.stl-mytests__row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 14px;
  width: 100%; text-align: left; padding: 13px 16px;
  border: 1px solid var(--stl-hairline); border-radius: var(--r-md);
  background: var(--stl-surface); color: var(--stl-text); font: inherit; cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.stl-mytests__row:hover { background: var(--stl-surface-2); border-color: var(--stl-accent-line); }
.stl-mytests__main { display: grid; gap: 2px; min-width: 0; }
/* A worksheet row has TWO actions (open, delete), so the row itself
 * can't be the button — the label becomes one instead. Buttons carry UA
 * chrome that a <span> doesn't, so a saved worksheet's title rendered as
 * a light-filled input-looking box. Strip it back to plain text. */
button.stl-mytests__main {
  appearance: none; -webkit-appearance: none;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
button.stl-mytests__main:focus-visible {
  outline: 2px solid rgba(var(--stl-rainbow-mid-rgb), 0.9);
  outline-offset: 3px;
  border-radius: 4px;
}
/* Rows built from a label + a trailing action rather than one big
 * button: no whole-row cursor/hover, since only the parts are clickable. */
.stl-mytests__row:has(> button.stl-mytests__main) { cursor: default; }
.stl-mytests__row:has(> button.stl-mytests__main):hover {
  background: var(--stl-surface); border-color: var(--stl-hairline);
}
.stl-mytests__row:has(> button.stl-mytests__main):focus-within,
.stl-mytests__row:has(> button.stl-mytests__main:hover) {
  background: var(--stl-surface-2); border-color: var(--stl-accent-line);
}
.stl-mytests__name {
  font-weight: 600; letter-spacing: -0.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.stl-mytests__meta { color: var(--stl-text-muted); font-size: 13px; font-variant-numeric: tabular-nums; }
.stl-mytests__status {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--stl-font-mono);
  font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--stl-text-muted); padding: 4px 10px;
  border: 1px solid var(--stl-hairline); border-radius: var(--r-pill);
  white-space: nowrap;
}
.stl-mytests__status::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--stl-text-subtle);
}
.stl-mytests__status[data-status="live"] { color: #4ade80; border-color: rgba(var(--stl-green-rgb), 0.28); }
.stl-mytests__status[data-status="live"]::before { background: var(--stl-green); }
.stl-mytests__chev { flex: none; color: var(--stl-text-subtle); transition: transform 120ms ease, color 120ms ease; }
.stl-mytests__row:hover .stl-mytests__chev { transform: translateX(2px); color: var(--stl-text); }
/* Review-screen "Graph the solution" button (reuses .stl-quiz-tool chrome). */
.stl-review__graph { margin-top: var(--s-3); }
/* Boot loading veil — covers the viewport during a quiz/review
 * refresh-resume so the picker doesn't flash while the bank loads. */
.stl-boot-veil {
  /* Sits BELOW the sticky header so the nav bar stays visible while the
   * saved screen loads; the spinner centers in the content area. `top`
   * defaults to the desktop header height and is refined to the exact
   * header height in JS (boot short-circuit). z-index is under the
   * header (50) and over the content screens. */
  position: fixed;
  top: calc(60px + env(safe-area-inset-top));
  left: 0; right: 0; bottom: 0;
  z-index: 40;
  display: grid; place-items: center;
  /* Two rows when the caption is in use (spinner + label). With a single
   * visible child this collapses to the same centered spinner as before. */
  grid-auto-rows: min-content;
  row-gap: 14px;
  align-content: center;
  background: var(--stl-bg);
}
.stl-boot-veil[hidden] { display: none; }
.stl-boot-veil__label {
  margin: 0;
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--stl-text-muted);
}
.stl-boot-veil__label[hidden] { display: none; }
.stl-boot-veil__spinner {
  /* Geometry only — the ring comes from the shared .stl-loader rule.
   * This was a border ring with an accent-colored top arc at 0.7s, and
   * it is the spinner with the widest reach: the boot veil, all five
   * admin pane veils, and the three Manage tab veils. Unifying it is
   * most of what "all the spinners are the same" means in practice. */
  --stl-loader-size: 30px;
  --stl-loader-thickness: 2px;
}
/* stl-boot-spin is gone: every spinner is on stl-spin now. */
#test-results[hidden] { display: none; }

/* ===== Targeted test from matrix cells (D4) ===== */
.stl-matrix__cell[data-cell] { cursor: pointer; user-select: none; }
.stl-matrix__cell[data-cell]:hover { background: var(--stl-accent-soft); }
.stl-matrix__cell.is-selected,
.stl-matrix__cell[aria-pressed="true"] {
  /* FIXLIST P2 (2026-08-18): was a 2px inset ring — rule 2's exact
   * prohibition, and the only selected-state ring left in the app. Selected
   * now reads as fill + colour, like every other control. */
  background: var(--stl-accent-soft);
  color: var(--stl-accent);
  font-weight: 700;
}
.stl-matrix__cell[data-cell]:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: -2px;
}
.stl-matrix__hint { margin-top: 8px; }
.stl-targeted {
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
  margin-top: 12px; padding: 14px;
  border: 1px solid var(--stl-accent); border-radius: var(--r-md);
  background: var(--stl-accent-soft);
}
.stl-targeted__count { font-weight: 600; }
.stl-targeted__field { display: flex; align-items: center; gap: 8px; }
.stl-targeted__field .stl-input { width: 80px; }
.stl-targeted .stl-composer__result { flex-basis: 100%; }

/* ===== Composer topics: full-width multi-select dropdown ===== */
.stl-topicdd { position: relative; }
.stl-topicdd__toggle {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 11px 14px; text-align: left; font: inherit; cursor: pointer; color: var(--stl-text);
  background: transparent; border: 1px solid var(--stl-hairline); border-radius: var(--r-md);
}
.stl-topicdd__toggle:hover { border-color: var(--stl-accent); }
.stl-topicdd__chev { flex: none; color: var(--stl-text-muted); transition: transform 140ms ease; }
.stl-topicdd__toggle[aria-expanded="true"] .stl-topicdd__chev { transform: rotate(180deg); }
.stl-topicdd__panel {
  margin-top: 6px; display: grid; gap: 8px; padding: 10px;
  border: 1px solid var(--stl-hairline); border-radius: var(--r-md);
  background: var(--stl-surface-2);
}
.stl-topicdd__panel[hidden] { display: none; }
.stl-topicdd__search { width: 100%; }
.stl-topicdd__actions { display: flex; gap: 16px; }
.stl-topicdd__list { max-height: 280px; overflow-y: auto; display: grid; gap: 2px; }
.stl-topicdd__opt {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  padding: 8px 10px; border: 0; border-radius: 8px; background: transparent; font: inherit;
  cursor: pointer; color: var(--stl-text);
}
.stl-topicdd__opt:hover { background: var(--stl-accent-soft); }
.stl-topicdd__opt.is-on,
.stl-topicdd__opt[aria-selected="true"] { font-weight: 600; }
.stl-topicdd__check {
  width: 18px; height: 18px; flex: none; display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--stl-text-subtle); border-radius: 5px;
  font-size: 12px; line-height: 1; color: #fff;
}
.stl-topicdd__opt.is-on .stl-topicdd__check { background: var(--stl-accent); border-color: var(--stl-accent); }
.stl-topicdd__empty { padding: 8px 10px; margin: 0; }

/* ===== Desmos-by-topic control (Tests panel) ========================= */
.stl-desmos-section {
  border: 1px solid var(--stl-hairline);
  border-radius: 14px;
  background: var(--stl-surface);
  margin-bottom: 1rem;
  overflow: hidden;
}
.stl-desmos-section[open] { background: var(--stl-surface-2); }
.stl-desmos-section__summary {
  list-style: none;
  cursor: pointer;
  padding: 0.9rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.stl-desmos-section__summary::-webkit-details-marker { display: none; }
.stl-desmos-section__title {
  font-weight: 600;
  color: var(--stl-text);
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  letter-spacing: 0.01em;
}
.stl-desmos-section__chevron {
  width: 0.5rem; height: 0.5rem;
  border-right: 1.5px solid var(--stl-text-subtle);
  border-bottom: 1.5px solid var(--stl-text-subtle);
  transform: rotate(-45deg);
  transition: transform 0.18s ease;
}
.stl-desmos-section[open] .stl-desmos-section__chevron { transform: rotate(45deg); }
.stl-desmos-section__hint { color: var(--stl-text-subtle); font-size: 0.82rem; }
.stl-desmos-section__body {
  border-top: 1px solid var(--stl-hairline);
  padding: 0.3rem 0.6rem 0.5rem;
  max-height: 22rem;
  overflow-y: auto;
}
.stl-desmos-section__empty { padding: 0.8rem 0.5rem; }
.stl-desmos-topic {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0.4rem;
  border-bottom: 1px solid var(--stl-hairline);
}
.stl-desmos-topic:last-child { border-bottom: 0; }
.stl-desmos-topic__meta { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.stl-desmos-topic__name { color: var(--stl-text); font-size: 0.9rem; }
.stl-desmos-topic__offlist {
  font-size: 0.68rem;
  color: var(--stl-text-subtle);
  border: 1px solid var(--stl-hairline);
  border-radius: 5px;
  padding: 0 0.3rem;
  margin-left: 0.3rem;
  vertical-align: middle;
}
.stl-desmos-topic__stat { color: var(--stl-text-subtle); font-size: 0.76rem; }
/* Segmented tri-state */
.stl-seg {
  display: inline-flex;
  flex: none;
  border: 1px solid var(--stl-hairline-hi);
  border-radius: 9px;
  overflow: hidden;
  background: var(--stl-bg-2);
}
.stl-seg__btn {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: transparent;
  color: var(--stl-text-muted);
  font: inherit;
  font-size: 0.78rem;
  padding: 0.32rem 0.62rem;
  cursor: pointer;
  transition: background 0.14s ease, color 0.14s ease;
}
.stl-seg__btn + .stl-seg__btn { border-left: 1px solid var(--stl-hairline); }
.stl-seg__btn:hover { color: var(--stl-text); }
.stl-seg__btn.is-active {
  background: var(--stl-accent-soft);
  color: var(--stl-text);
  font-weight: 500;
}
.stl-seg__btn:focus-visible { outline: 2px solid var(--stl-accent); outline-offset: -2px; }

/* ============================================================
   Part 5 — results redesign (Mike 2026-06-27): mastery-led hero,
   Misconception callout, "Your Results" domain bars, Run the Signal.
   Theme-token driven; the only fixed colors are the signal three.
   ============================================================ */

/* Misconception callout REMOVED at every count (Mike item 12, 2026-07-06 —
 * supersedes the 2026-07-02 zero-only rule): misconceptions always lead
 * inside Run the Signal, so the banner was redundant. Ledger row remains. */

/* Results action cluster, now under the DOMAIN BARS (2026-08-06) rather
 * than under the overview. Mike (2026-07-02 QA): the link treatment still
 * read as plain text — these are real BUTTONS, secondary/outlined style,
 * still visually quieter than the "Run the Signal" primary (no signal
 * dots, muted border).
 *
 * Two-up grid, not a single stack. Five stacked buttons ran ~227px; the
 * same five in two columns run ~133px, which is what closes the last of
 * the column imbalance (23% dead space down to single digits). "Walk
 * through review" spans the full row because it is the one action here
 * anybody actually takes; the other four are utilities and read fine at
 * half width. The ≤640px block below flattens this back to a wrapped row
 * of text links, so the grid is desktop-only. */
/* Icon quick-actions (2026-08-26, Josh). Share-sheet anatomy: a circled
 * 19px stroke icon over an 11.5px caption. One even five-up row replaces
 * the old worksheet-spans-both 2×2 pill grid — the icons are what let
 * five actions share a row without the row deciding a hierarchy.
 * Hover is colour/fill ONLY (§5.7: nothing here may change a dimension),
 * and the ring is the standard focus treatment, on the circle so the
 * visible focus matches the visible control. */
.stl-results__quietlinks {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  align-items: start;
  gap: 8px; margin-top: 14px;
}
.stl-results__qlink {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  width: 100%; padding: 8px 2px 6px; cursor: pointer;
  background: none; border: 0; border-radius: var(--r-md);
  color: var(--stl-text-muted); font: inherit;
  transition: color .15s ease, transform .05s ease;
}
.stl-results__qlink-ico {
  display: grid; place-items: center;
  width: 44px; height: 44px; flex: none;
  border-radius: var(--r-pill);
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline-hi);
  transition: background .15s ease, border-color .15s ease;
}
.stl-results__qlink-ico svg { width: 19px; height: 19px; }
.stl-results__qlink-lbl {
  font-size: 11px; font-weight: 550; letter-spacing: 0;
  text-align: center; line-height: 1.25; max-width: 100%;
  /* Wrap, never ellipsise: "Workshe…" tells the student nothing. Two-word
   * captions break to two lines like a native share sheet; align-items:
   * start on the row keeps neighbours from stretching to match. */
}
.stl-results__qlink:hover:not(:disabled) { color: var(--stl-text); }
.stl-results__qlink:hover:not(:disabled) .stl-results__qlink-ico {
  background: var(--stl-surface-2);
  border-color: var(--stl-text-faint);
}
.stl-results__qlink:active:not(:disabled) { transform: translateY(1px); }
.stl-results__qlink:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
.stl-results__qlink:disabled { color: var(--stl-text-faint); cursor: default; }
.stl-results__qlink:disabled .stl-results__qlink-ico {
  background: none;
  border-color: var(--stl-hairline);
}
@media (max-width: 420px) {
  /* Five captions across a 390 card: shave the caption, never the icon. */
  .stl-results__qlink-lbl { font-size: 10.5px; }
}

/* "Your Results" — domain bars (right column of .stl-results__top). */
.stl-results__domains-section { display: flex; flex-direction: column; gap: 10px; margin: 0; min-width: 0; }
.stl-results__domains-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.stl-results__domains-hint { font-size: .72rem; color: var(--stl-text-subtle); }
.stl-results__domains { display: flex; flex-direction: column; gap: 8px; }
.stl-results__domains-empty { color: var(--stl-text-subtle); font-size: .9rem; }
.stl-results__domain { border: 1px solid var(--stl-hairline); border-radius: var(--r-md); background: var(--stl-surface); overflow: hidden; }
.stl-results__domain-head { display: flex; flex-direction: column; gap: 8px; width: 100%; padding: 11px 13px; background: none; border: 0; cursor: pointer; text-align: left; color: var(--stl-text); }
.stl-results__domain-head:hover { background: var(--stl-surface-2); }
.stl-results__domain-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.stl-results__domain-name { font-weight: 600; }
.stl-results__domain-meta { display: inline-flex; align-items: center; gap: 8px; color: var(--stl-text-muted); font-size: .85rem; }
.stl-results__domain-q { color: var(--stl-text-subtle); }
.stl-results__domain-pct { font-variant-numeric: tabular-nums; font-weight: 650; }
.stl-results__domain-caret { transition: transform .15s ease; color: var(--stl-text-subtle); font-size: .8rem; }
.stl-results__domain-head[aria-expanded="true"] .stl-results__domain-caret { transform: rotate(90deg); }
.stl-results__domain-bar { height: 6px; border-radius: var(--r-pill); background: var(--stl-surface-3); overflow: hidden; }
.stl-results__domain-fill { display: block; height: 100%; border-radius: var(--r-pill); transition: width .4s cubic-bezier(.22,.61,.36,1); }
.stl-results__domain[data-band="green"] .stl-results__domain-fill { background: var(--stl-green); }
.stl-results__domain[data-band="green"] .stl-results__domain-pct { color: var(--stl-green-text); }
.stl-results__domain[data-band="amber"] .stl-results__domain-fill { background: var(--stl-yellow); }
.stl-results__domain[data-band="amber"] .stl-results__domain-pct { color: var(--stl-yellow-text); }
.stl-results__domain[data-band="red"] .stl-results__domain-fill { background: var(--stl-red); }
.stl-results__domain[data-band="red"] .stl-results__domain-pct { color: var(--stl-red-text); }
/* drill-down topic rows — collapsed by default (writeStats renders them
 * [hidden]); the explicit rule is required because `display: flex` outranks
 * the UA [hidden] default, which shipped every domain pre-expanded
 * (Mike, 2026-07-02 + 2026-07-06). */
.stl-results__dtopics[hidden] { display: none; }
.stl-results__dtopics { list-style: none; margin: 0; padding: 2px 13px 11px; display: flex; flex-direction: column; gap: 5px; border-top: 1px solid var(--stl-hairline); }
.stl-results__dtopic { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: .85rem; }
.stl-results__dtopic-name { color: var(--stl-text-muted); min-width: 0; }
.stl-results__dtopic-counts { display: inline-flex; gap: 10px; font-variant-numeric: tabular-nums; flex: none; }
.stl-results__dtopic-r { color: var(--stl-green-text); }
.stl-results__dtopic-w { color: var(--stl-red-text); }

/* Run the Signal spans the full width of the column it closes (Josh
 * 2026-08-06). It is the one loud action on the results screen, and a
 * centered auto-width pill under a full-width grid of quiet links read
 * like an afterthought. Full width also lines its edges up with the
 * domain bars and the button grid directly above it. */
/* .stl-results__runsignal-row is gone (2026-08-07) — the Signal CTA is
 * a direct child of .stl-results__footer-actions now.
 *
 * Rev 3: the pair lives OUTSIDE .stl-results__top, so it spans the full
 * card width under both columns. It needs more air above it than the
 * default action-row margin, because it now clears the taller of the
 * two columns and reads as its own section. */
.stl-results__footer-actions { margin-top: 26px; }
/* Match the back half's box so the two sides are the same height. The
 * base .stl-btn--signal is a hug-content pill; inside the pair it fills
 * its cell. nowrap because the label plus signal dots plus count pill
 * wrapped to three lines when the pair was still stuck in the right
 * column — full width fixes that, but a long test name or a bigger
 * count should not be able to bring it back. */
/* Signal review and Walkthrough share EVERY rule below (Joshua
 * 2026-08-16). Not two rules kept in step — one selector list, so the
 * two halves of the results mode pair cannot drift apart. The first
 * version gave --mode its own block with matching padding, and the
 * button still rendered as bare text beside a pill because the block
 * did not repeat the background and border. The only intended
 * difference is the signal dots, which only Signal review carries. */
.stl-actionpair > .stl-btn--signal,
.stl-actionpair > .stl-btn--mode {
  width: 100%;
  justify-content: center;
  padding: 13px 20px;
  white-space: nowrap;
}
.stl-btn--signal,
.stl-btn--mode {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 12px 26px; border-radius: var(--r-pill);
  background: var(--stl-surface); border: 1px solid var(--stl-hairline-hi);
  color: var(--stl-text); font: inherit; font-weight: 650; cursor: pointer;
  transition: background .15s ease, border-color .15s ease, transform .05s ease;
}
.stl-btn--signal:hover:not(:disabled),
.stl-btn--mode:hover:not(:disabled) { background: var(--stl-surface-2); border-color: var(--stl-text-faint); }
.stl-btn--signal:active,
.stl-btn--mode:active { transform: translateY(1px); }
.stl-btn--signal:disabled,
.stl-btn--mode:disabled { opacity: .5; cursor: default; }
.stl-signal-dots { display: inline-flex; gap: 4px; }
.stl-signal-dots i { width: 8px; height: 8px; border-radius: 50%; display: block; }
.stl-signal-dots i:nth-child(1) { background: var(--stl-green); }
.stl-signal-dots i:nth-child(2) { background: var(--stl-yellow); }
.stl-signal-dots i:nth-child(3) { background: var(--stl-red); }

/* ============================================================
   Part 4 — "Run the Signal" guided session screen (Mike 2026-06-27).
   Self-contained; reuses .stl-choice + signal tokens.
   NOTE: the container is `.stl-rts`, NOT `.stl-signal` — `.stl-signal` is the
   quiz's green/yellow/red confidence group (a 3-col grid), and reusing that
   class stacked the quiz cards vertically. Keep these namespaces separate.
   ============================================================ */
/* No max-width (2026-08-06): this sits inside .stl-main, whose content box
 * is 760px minus up to 32px of padding each side, so a 720px cap here could
 * never bind. Removing it keeps page width in one place. */
.stl-rts { margin: 0 auto; display: flex; flex-direction: column; gap: 14px; }
.stl-signal__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.stl-signal__head-left { display: inline-flex; align-items: center; gap: 10px; }
.stl-signal__stage { font-weight: 650; font-size: 1.05rem; }
.stl-signal__head-right { display: inline-flex; align-items: center; gap: 14px; }
/* Fixed width so the counter cannot shove the toggle sideways when the
 * denominator gains a digit on flip — the whole row would twitch on a
 * control whose entire job is to change that number. */
.stl-signal__progress-text {
  font-size: .82rem; color: var(--stl-text-muted);
  font-variant-numeric: tabular-nums;
  min-width: 5em; text-align: right;
}
/* View all (2026-08-20) borrows `.stl-admin__pill-toggle` wholesale —
 * the ONLY additions are a height that matches this header's scale and
 * a disabled state the admin instance never needed (a perfect run has
 * nothing to filter out, so the control cannot be turned off). */
.stl-signal__viewall { height: 32px; padding: 0 12px; font-size: 12.5px; }
.stl-signal__viewall:disabled { opacity: .45; cursor: default; }
.stl-signal__viewall:disabled:hover {
  border-color: var(--stl-hairline);
  color: var(--stl-text-subtle);
}
@media (max-width: 560px) {
  /* Phones: the label goes, the dot stays. Same trade the header's own
   * toggles make — the aria-label carries the meaning either way. */
  .stl-signal__viewall .stl-admin__pill-toggle__text { display: none; }
  .stl-signal__viewall { padding: 0 10px; }
  .stl-signal__head-right { gap: 10px; }
}
.stl-signal__track { height: 6px; border-radius: var(--r-pill); background: var(--stl-surface-3); overflow: hidden; }
.stl-signal__track-fill { display: block; height: 100%; width: 0; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--stl-green), var(--stl-yellow)); transition: width .4s cubic-bezier(.22,.61,.36,1); }
.stl-signal__body { display: flex; flex-direction: column; }
.stl-signal__card { display: flex; flex-direction: column; gap: 14px; padding: 20px; border: 1px solid var(--stl-hairline); border-radius: var(--r-lg); background: var(--stl-surface); }
.stl-signal__eyebrow { font-size: .8rem; color: var(--stl-text-muted); margin: 0; }
.stl-signal__topic { font-size: 1.15rem; font-weight: 650; margin: 0; }
.stl-signal__muted { color: var(--stl-text-muted); font-size: .9rem; }
.stl-signal__lesson { display: flex; flex-direction: column; gap: 12px; }
.stl-signal__lesson-stem { font-size: 1.05rem; line-height: 1.5; }
.stl-signal__lesson-expl { padding: 14px 16px; background: var(--stl-surface-2); border-radius: var(--r-md); border: 1px solid var(--stl-hairline); line-height: 1.55; }
/* Centered under the problem (Mike 2026-07-25) — left-aligned looked
 * off on wide problems. */
.stl-signal__actions { display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap; }
/* Forward motion lives on the right (Josh 2026-08-17). auto margin, not
 * space-between, so the left-hand actions stay grouped when View
 * Explanation is absent (it disappears once revealed). */
.stl-signal__actions-next { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; }
@media (max-width: 560px) {
  .stl-signal__actions-next { margin-left: 0; width: 100%; justify-content: center; }
}
.stl-signal__qlink { background: none; border: 0; padding: 6px 0; cursor: pointer; color: var(--stl-text-muted); font: inherit; font-size: .9rem; border-bottom: 1px solid transparent; }
.stl-signal__qlink:hover { color: var(--stl-text); border-bottom-color: var(--stl-hairline-hi); }
/* Meta row must WRAP (Mike mobile bug 2026-07-26): topic + state chip +
 * tier + Reference/Calculator are all nowrap chips totaling ~450px —
 * unwrapped they inflated the card past a phone viewport and the whole
 * page side-scrolled (stem, buttons, and sticky bar all sheared). */
.stl-signal__qmeta { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; row-gap: 6px; font-size: .82rem; color: var(--stl-text-muted); }
.stl-signal__qmeta-left, .stl-signal__qmeta-right { display: inline-flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: wrap; row-gap: 6px; }
.stl-signal__card { max-width: 100%; min-width: 0; }
/* The tier was bare coloured text beside a bordered state chip, so it
 * read as a word floating in the row rather than a peer of the chip
 * next to it — and it printed the raw key ("insane"). It is a
 * .stl-pill now, inheriting the app-wide difficulty vocabulary via
 * [data-d], with a capitalised label (Josh 2026-08-17). */
.stl-signal__tier { letter-spacing: .02em; }
/* Category chip inside signal headings — keep it from inflating the h2. */
.stl-signal__topic .stl-pill { vertical-align: middle; margin-left: 6px; }
.stl-signal__figure:empty { display: none; }
.stl-signal__stem { font-size: 1.1rem; line-height: 1.5; }
.stl-signal__choices { display: flex; flex-direction: column; gap: 8px; }
.stl-signal__choices .stl-choice.is-selected { border-color: rgb(var(--stl-tint, 139 134 255)); }
.stl-signal__gridin { width: 100%; padding: 12px 14px; border-radius: var(--r-md); border: 1px solid var(--stl-hairline-hi); background: var(--stl-surface-2); color: var(--stl-text); font: inherit; }
.stl-signal__help { font-size: .85rem; color: var(--stl-text-muted); margin: 4px 0 0; }
.stl-signal__signals { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.stl-signal__sig { padding: 12px; border-radius: var(--r-md); border: 1px solid var(--stl-hairline-hi); background: var(--stl-surface); color: var(--stl-text); font: inherit; font-weight: 600; cursor: pointer; transition: background .12s ease, border-color .12s ease, transform .05s ease; }
/* Stacked label + colour subtext, mirroring the quiz buttons
 * (.stl-signal__lbl / __sub). These are the Signal review trainer's own
 * controls; a student answering here was being shown a different
 * vocabulary from the test they had just finished. */
.stl-signal__sig { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.stl-signal__siglbl { font-weight: 600; line-height: 1.25; }
/* Deliberately identical to .stl-signal__sub (the quiz button's subtext,
 * ~line 4126) rather than its own treatment: same size, muted by COLOUR so
 * the label keeps the signal tint on its own. The first version here was
 * uppercase and dimmed by opacity, which meant the same control read
 * "Know it / GREEN" in review and "Know it / Green" on the test — one
 * vocabulary in two voices, which is the thing this change set out to fix. */
.stl-signal__sigsub { font-size: 0.8rem; font-weight: 500; line-height: 1.35;
  color: var(--stl-text-muted); }
.stl-signal__sig:hover { background: var(--stl-surface-2); }
.stl-signal__sig:active { transform: translateY(1px); }
.stl-signal__sig--g { border-color: var(--stl-green); color: var(--stl-green-text); }
.stl-signal__sig--y { border-color: var(--stl-yellow); color: var(--stl-yellow-text); }
.stl-signal__sig--r { border-color: var(--stl-red); color: var(--stl-red-text); }
.stl-signal__donecard { align-items: center; text-align: center; gap: 10px; }
.stl-signal__donecard .stl-signal-dots { transform: scale(1.4); margin-bottom: 6px; }
/* Stage-entry interstitials + the misconception explanation gate — one
 * loud primary, quiet secondary underneath (spec Part 4). */
.stl-signal__introcard { align-items: flex-start; gap: 10px; }
.stl-signal__introcard .stl-signal__actions { margin-top: 6px; flex-direction: column; align-items: flex-start; gap: 10px; }

/* ====================================================================
 * Baseline Assessment — entry card, scope tiles, results banner
 * (2026-07-12). The assess screen leans on the picker-tile + score-
 * section vocabulary; everything here is just the deltas.
 * ==================================================================== */

/* "Not sure where to start?" entry card on the picker screen. Reads as
 * a quieter sibling of the test tiles — hairline border, no tint glow,
 * full-width row under the grid. */
/* Site-wide legal footer (Mike 2026-07-25): College Board trademark
 * notice + Desmos attribution. Quiet, centered, on every screen. */
.stl-sitefoot {
  /* The footer sits OUTSIDE .stl-main (index.html, after </main>), so it
   * cannot inherit the shell — it has to restate it. It tracked
   * --shell-read while it was one legal line under a 760px column.
   * Now it carries five link columns, so it takes the DATA width, the
   * same token the wide marketing screens use. Still a shell token,
   * still one declaration (container-widths.test.js pins both). */
  max-width: var(--shell-data);
  /* Same flex-item caveat as .stl-main above: auto side margins beat the
   * cross-axis stretch, so without this the footer shrink-wraps and
   * re-centers itself against whatever width its content happens to need. */
  width: 100%;
  margin: 48px auto 0;
  padding: 32px 20px 36px;
  font-size: 11px;
  line-height: 1.6;
  color: var(--stl-text-subtle);
  border-top: 1px solid var(--stl-hairline);
}
.stl-sitefoot__cols {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 28px 20px;
  text-align: left;
  margin-bottom: 32px;
}
.stl-sitefoot__h {
  margin: 0 0 10px;
  font-family: var(--stl-font-mono);
  font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--stl-text-faint);
}
.stl-sitefoot__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.stl-sitefoot__link {
  font-size: 13px;
  color: var(--stl-text-muted);
  text-decoration: none;
  transition: color var(--dur-base) var(--ease-out);
}
.stl-sitefoot__link:hover { color: var(--stl-text); }
/* Fixed-width name slot: PSAT/ISEE/SSAT/HSPT set in a proportional face
 * are all different widths, so trailing Soon chips landed jagged. The
 * slot pins every chip to one column, left AND right edges (2026-08-26). */
.stl-sitefoot__testname { display: inline-block; min-width: 3.1em; }
/* "Soon" is conventional caution, one of the three sanctioned uses of
 * the signal palette — it is a status, not decoration. Composed from
 * the token so light mode follows. */
.stl-sitefoot__soon {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: var(--r-pill);
  font-family: var(--stl-font-mono);
  font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--stl-yellow-text);
  background: rgba(var(--stl-yellow-rgb), 0.12);
  border: 1px solid rgba(var(--stl-yellow-rgb), 0.28);
}
.stl-sitefoot__legal { margin: 0; text-align: center; }

@media (max-width: 860px) {
  .stl-sitefoot__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 26px 18px; }
}
@media (max-width: 420px) {
  .stl-sitefoot__cols { grid-template-columns: minmax(0, 1fr); }
}
/* Score-band estimate disclaimer on diagnostic results. */
.stl-band-note {
  display: block;
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--stl-text-subtle);
}
/* Estimated-score emphasis (Mike 2026-07-26): small label, BIG band
 * number in the current test's accent color, muted raw count. The
 * accent var is set per-test by applyTestAccent, so SAT shows purple
 * and PSAT blue automatically. */
.stl-band-label {
  font-weight: 600;
  color: var(--stl-text);
}
.stl-band-score {
  font-size: 2em;
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--stl-accent);
  vertical-align: -0.08em;
  margin: 0 2px;
}
.stl-band-raw {
  color: var(--stl-text-subtle);
  font-size: 0.95em;
}

/* Bonus Tips divider + chip on the lesson index (Mike 2026-07-25). */
.stl-learn__bonushdr {
  margin: 18px 0 4px;
  padding-top: 14px;
  border-top: 1px solid var(--stl-hairline);
  display: grid;
  gap: 3px;
}
.stl-learn__bonuslbl {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--stl-accent);
}
.stl-learn__bonussub {
  font-size: 12.5px;
  color: var(--stl-text-muted);
  line-height: 1.5;
}
.stl-learn__bonuschip {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 8px;
  border-radius: var(--r-pill);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stl-accent);
  border: 1px solid currentColor;
  vertical-align: middle;
}

/* Practice-question tool row (Mike 2026-07-25): meta on the left,
 * calculator button top-right — mirrors the live test screen. */
.stl-learn__q-toolrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}
.stl-learn__q-toolrow .stl-learn__q-meta { margin: 0; }

/* Learn-screen back button — top-left above the H1 (Mike 2026-07-25).
 * Rev 2: the button base is full-width flex, which stretched it across
 * the whole card — shrink to content width, hugging the left, same
 * style otherwise. */
.stl-learn__back {
  display: inline-flex;
  width: auto;
  align-self: flex-start;
  margin-bottom: var(--s-4);
}

/* ── "Why it works" band on the picker (Josh 2026-08-05) ────────────
 * Replaces the old .stl-picker__whylist, which was three full-sentence
 * <li> rows stacked INSIDE the picker card between the H1 and the test
 * tiles. Josh's note was "it's a wall of text", and the measurement
 * backed it: ~90 words of body copy between the headline and the thing
 * people came to click.
 *
 * The fix is structural, not typographic. The content moved OUT of the
 * card and above it, so the card is headline-then-tiles again, and the
 * three paragraphs became three columns of one strip. Each feature's
 * noun phrase is promoted to a title and its sentence trimmed to one
 * line, so the same three claims survive but stop reading as prose.
 *
 * Why ONE strip and not three cards: three cards means three borders
 * and three padding boxes, which is roughly 30% more vertical space
 * for the same words, and it reads as three things being sold. One
 * band with hairline dividers reads as a note the product is making.
 *
 * The rainbow border is the same padding-box / border-box double
 * background used by --pill-rainbow and (since 2026-08-05) by
 * .stl-btn--primary. Fill is hardcoded #0d0e18 dark; the light-mode
 * swap lives with the rest of the rainbow-border group near the top of
 * this file. Add any new adopter there, not scattered.
 *
 * Hidden below 640px, along with its re-open link. Phones already get
 * this material from .stl-mland__whylist on the landing, and on the
 * .is-phome living home the picker card itself is hidden. See the
 * media query near the bottom of this file. */
.stl-whyband {
  position: relative;
  margin: 0 0 var(--gap-normal);
  padding: 16px 46px 16px 18px;
  border-radius: 14px;
  background:
    linear-gradient(#0d0e18, #0d0e18) padding-box,
    var(--stl-rainbow) border-box;
  /* 1.5px matches .stl-btn--primary. A 1px four-stop gradient turns to
   * mush once it has to bend around a 14px radius. */
  border: 1.5px solid transparent;
  /* Own GPU layer. Gradient borders on rounded corners can show
   * subpixel artifacts when composited against a parent that has a
   * backdrop-filter, and this sits directly under the navbar. */
  transform: translate3d(0, 0, 0);
}
.stl-whyband__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto auto;
  gap: 0;
}
.stl-whyband__col {
  padding: 0 16px;
  border-left: 1px solid var(--stl-hairline);
}
/* Two of the three titles wrap to a second line at this column width
 * ("The Desmos Advantage", "Build your own worksheet") and one does not,
 * so with plain auto rows the three descriptions started at three
 * different heights and the band read as ragged.
 *
 * Subgrid fixes it without a magic min-height: each column adopts the
 * band's own two rows, so the title row is as tall as the tallest title
 * and all three descriptions share one baseline. Guarded by @supports
 * because the `span 2` would generate implicit rows if subgrid were
 * dropped; without it the columns simply fall back to the ragged-but-
 * readable layout rather than breaking. */
@supports (grid-template-rows: subgrid) {
  .stl-whyband__col {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 2;
  }
}
/* First column hugs the band's own padding instead of adding its own,
 * and has no divider to its left. */
.stl-whyband__col:first-child { padding-left: 0; border-left: 0; }
/* ==================== "How it works" page ==========================
 * Rebuilt 2026-08-10 (Josh) as a full marketing page; widened to the
 * data shell same day. Hero (two-column, CSS art), problem twins,
 * live demo, three steps, the six-outcome ledger, pillar cards,
 * teacher band, FAQ, closing CTA. how-page.test.js pins the pillar
 * ledes, the six <b>outcome</b> labels, and several selectors here.
 *
 * Motion: [data-rev] blocks fade-rise in via an IntersectionObserver
 * in app.js; [data-rev-child] items stagger inside a revealed block.
 * All of it collapses to no-ops under prefers-reduced-motion. */

/* Marketing pages don't live in a reading column: the how screen gets
 * the widest shell (same :has() pattern as admin/teacher) and sheds
 * the hero-card chrome — each band draws its own surface instead.
 * NO overflow clip here: .stl-app's own overflow-x: clip already
 * guards the horizontal scrollbar at the viewport, and clipping at
 * the card sliced the floating chips' glow flat at the shell edge
 * (Josh 2026-08-10). */
/* DESKTOP-ONLY on purpose (Josh 2026-08-26: "page looks off center on
 * responsive Safari"). This :has() selector outranks the <=640 phone
 * rule (.stl-main { max-width: none; margin: 0 }) by specificity, so on
 * phones it silently re-imposed a 100vw-32px width on a margin-less
 * main: pinned left, a 32px hole on the right, every section 16px off
 * centre. Media queries do not enter the cascade, so the ONLY way to
 * let the phone rule win is to not apply this one there. */
@media (min-width: 641px) {
  .stl-main:has([data-screen="how"]:not([hidden])),
  .stl-main:has([data-screen="pricing"]:not([hidden])),
  .stl-main:has([data-screen="faq"]:not([hidden])) {
    max-width: min(var(--shell-data), calc(100vw - 32px));
  }
}
.stl-card--hero.stl-how {
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
}
/* Light mode re-applies the hero-card shadow at higher specificity
 * (:root[data-theme] + class beats two classes), which drew a card
 * edge around the whole marketing page (Josh 2026-08-10). Kill it at
 * matching specificity. */
:root[data-theme="light"] .stl-card--hero.stl-how {
  background: none;
  border: 0;
  box-shadow: none;
}

.stl-how [data-rev] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.stl-how [data-rev].is-rev { opacity: 1; transform: none; }
.stl-how [data-rev-child] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.stl-how .is-rev [data-rev-child] { opacity: 1; transform: none; }
.stl-how .is-rev [data-rev-child]:nth-child(1) { transition-delay: 0.10s; }
.stl-how .is-rev [data-rev-child]:nth-child(2) { transition-delay: 0.22s; }
.stl-how .is-rev [data-rev-child]:nth-child(3) { transition-delay: 0.34s; }
.stl-how .is-rev [data-rev-child]:nth-child(4) { transition-delay: 0.46s; }
.stl-how .is-rev [data-rev-child]:nth-child(5) { transition-delay: 0.58s; }
.stl-how .is-rev [data-rev-child]:nth-child(6) { transition-delay: 0.70s; }

/* ---- hero: copy left, product art right ---- */
.stl-how__hero {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  align-items: center;
  gap: clamp(28px, 5vw, 64px);
  padding: clamp(20px, 4vw, 56px) 0 clamp(36px, 5vw, 64px);
}
.stl-how__h1 {
  margin: 0 0 18px;
  max-width: 16ch;
  font-family: var(--stl-font-display);
  font-size: clamp(34px, 4.6vw, 58px);
  font-weight: 700; letter-spacing: -0.025em; line-height: 1.05;
  color: var(--stl-text);
}
/* The one word the whole page turns on, wearing the signal itself.
 * Runs red → yellow → green so it LANDS on green: the word "known" is
 * the destination, and the ramp is the wordmark's own dot order.
 * Built from the -text pastel variants, not the mid-tones — at 58px
 * the saturated yellow goes muddy against the dark canvas, and these
 * are the pair that already swap correctly for light mode. */
/* Rotating hero title (Josh 2026-08-11): three claims crossfade on a
 * grid stack, so the tallest one reserves the height and the layout
 * never jumps. JS cycles .is-on; reduced motion keeps the first. */
.stl-how__h1--rotator { display: grid; }
.stl-how__h1-alt {
  grid-area: 1 / 1;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .7s ease, transform .7s ease;
  pointer-events: none;
}
.stl-how__h1-alt.is-on { opacity: 1; transform: none; pointer-events: auto; }
/* Orphan control (Josh 2026-08-12): balance the heading lines instead
 * of tying words — nbsp ties dropped words mid-line, which read worse.
 * text-wrap: balance evens the line lengths so no word strands. */
.stl-how__h1-alt, .stl-how__h2 { text-wrap: balance; }
/* Titles 2 and 3 run longer — they step down ~2pt (Josh 2026-08-11);
 * the first keeps the original scale. */
.stl-how__h1-alt:nth-child(2),
.stl-how__h1-alt:nth-child(3) { font-size: clamp(31px, 4.3vw, 55px); }
@media (prefers-reduced-motion: reduce) {
  .stl-how__h1-alt { transition: none; transform: none; }
}
.stl-how__h1 em {
  font-style: normal;
  /* Stops pulled in from the edges: across a 5-letter word a 0/50/100
   * ramp spends its ends mid-blend and the green never actually
   * arrives. These land each hue on a letter. */
  background: linear-gradient(100deg,
    var(--stl-red-text)     4%,
    var(--stl-yellow-text) 46%,
    var(--stl-green-text)  86%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.stl-how__hero-sub {
  margin: 0 0 28px;
  max-width: 50ch;
  font-size: clamp(15.5px, 1.4vw, 17.5px); line-height: 1.62;
  color: var(--stl-text-muted);
}
/* Hand-drawn underline (Josh 2026-08-26) — the annotation grammar the
 * Try-it-out arrow established: a wobbly stroke, not text-decoration.
 * The SVG hangs from the phrase's baseline and never affects layout. */
.stl-how__uline { position: relative; white-space: nowrap; }
.stl-how__uline svg {
  position: absolute; left: -2%; bottom: -0.42em;
  width: 104%; height: 0.5em;
  /* Same ink as the Try-it-out annotation (text-muted), so the two
   * hand-drawn marks on this screen read as one hand. */
  color: var(--stl-text-muted);
  pointer-events: none;
}
.stl-how__hero-cta { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 28px; }
/* Hero CTAs run a size up from the app chrome (Josh 2026-08-10) —
 * this is a marketing page and they are THE action. */
.stl-how__hero-cta .stl-btn { font-size: 16.5px; padding: 14px 28px; }
/* Closing CTA button matches the hero's size (Josh 2026-08-11). */
.stl-how__cta #btn-how-start-2 { font-size: 16.5px; padding: 14px 28px; }
/* Test list (Josh 2026-08-12): no pills — plain names separated by em
 * dots, each one a real button into that test's flow. */
.stl-how__scope { display: flex; align-items: center; gap: 0; flex-wrap: wrap; }
.stl-how__scope button {
  appearance: none; -webkit-appearance: none;
  border: 0; background: none; padding: 3px 0; margin: 0;
  font-family: var(--stl-font-mono);
  font-size: 11.5px; letter-spacing: 0.09em;
  color: var(--stl-text-subtle);
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-spring);
}
.stl-how__scope button:hover { color: var(--stl-text); }
.stl-how__scope button:focus-visible { outline: 2px solid var(--stl-accent); outline-offset: 3px; border-radius: 4px; }
/* Separator rides the gap between buttons, so no stray leading dot. */
.stl-how__scope button + button::before {
  content: "·";
  margin: 0 10px;
  color: var(--stl-text-faint);
}

/* ---- hero art: aurora + tilted mock card + floating chips ---- */
.stl-how__art {
  position: relative;
  min-height: 440px;
  display: flex; align-items: center; justify-content: center;
}
.stl-how__aurora {
  position: absolute;
  inset: -60px;
  pointer-events: none;
  /* A 3D scene for the blobs below. `perspective` as a PROPERTY (not a
     transform function) so it applies to the children while this element
     keeps its own transform free for the counter-lift. */
  perspective: 1200px;
  transform-style: preserve-3d;
  transition: transform 460ms cubic-bezier(0.22, 0.68, 0.24, 1);
}
/* Josh 2026-08-18: the lights are not attached to the card. `.stl-how__art`
 * translates up when the verdict sheet opens, and the aurora is inside it,
 * so it was riding along — which made the background read as part of the
 * component. Cancelling the parent's lift with an equal and opposite
 * translate pins the glow to the page while the card moves against it.
 * Same easing and duration, so the two cancel exactly at every frame rather
 * than only at the endpoints. */
.stl-how__art.is-demo-lifted .stl-how__aurora {
  transform: translateY(var(--demo-lift, 0px));
}
/* Eased radial fills instead of blurred solids (Josh 2026-08-10,
 * grain removal): a solid circle through blur(70px) still lands as a
 * near-linear alpha ramp that bands, and it needed a grain overlay to
 * hide the rings. The half-cosine stop ramp (see the .stl-app note)
 * has zero slope at both ends, so it needs neither the blur nor the
 * noise — and drops the filter compositing cost. Blobs are ~1.6x
 * bigger because the gradient fades inside the box instead of
 * bleeding past it. */
.stl-how__aurora i {
  position: absolute;
  border-radius: 50%;
}
.stl-how__aurora i:nth-child(1) {
  /* NO z-index here: lifting this blob painted it over the mock card
   * and floating chips (the green-washed Mastery pill, Josh
   * 2026-08-10). DOM order already keeps the aurora under the card. */
  width: 540px; height: 540px; top: -140px; left: -160px;
  background: radial-gradient(closest-side,
    rgba(var(--stl-green-rgb), 0.30)  0%,
    rgba(var(--stl-green-rgb), 0.284) 15%,
    rgba(var(--stl-green-rgb), 0.238) 30%,
    rgba(var(--stl-green-rgb), 0.173) 45%,
    rgba(var(--stl-green-rgb), 0.104) 60%,
    rgba(var(--stl-green-rgb), 0.044) 75%,
    rgba(var(--stl-green-rgb), 0.007) 90%,
    rgba(var(--stl-green-rgb), 0)     100%);
  animation: stlAuroraOrbitY 62s linear infinite;
}
.stl-how__aurora i:nth-child(2) {
  /* Peak lifted 0.20 → 0.30 (Josh 2026-08-10): the slate ambient orb
   * occupies the same corner and was washing the yellow out — the
   * aurora paints ABOVE the orbs, but at equal strength it read as
   * sitting behind the gray. */
  width: 480px; height: 480px; top: 10%; right: -180px;
  background: radial-gradient(closest-side,
    rgba(var(--stl-yellow-rgb), 0.30)  0%,
    rgba(var(--stl-yellow-rgb), 0.284) 15%,
    rgba(var(--stl-yellow-rgb), 0.238) 30%,
    rgba(var(--stl-yellow-rgb), 0.173) 45%,
    rgba(var(--stl-yellow-rgb), 0.104) 60%,
    rgba(var(--stl-yellow-rgb), 0.044) 75%,
    rgba(var(--stl-yellow-rgb), 0.007) 90%,
    rgba(var(--stl-yellow-rgb), 0)     100%);
  animation: stlAuroraOrbitXY 74s linear -18s infinite;
}
.stl-how__aurora i:nth-child(3) {
  width: 500px; height: 500px; bottom: -160px; left: 4%;
  background: radial-gradient(closest-side,
    rgba(var(--stl-red-rgb), 0.22)  0%,
    rgba(var(--stl-red-rgb), 0.208) 15%,
    rgba(var(--stl-red-rgb), 0.175) 30%,
    rgba(var(--stl-red-rgb), 0.127) 45%,
    rgba(var(--stl-red-rgb), 0.076) 60%,
    rgba(var(--stl-red-rgb), 0.032) 75%,
    rgba(var(--stl-red-rgb), 0.005) 90%,
    rgba(var(--stl-red-rgb), 0)     100%);
  animation: stlAuroraOrbitX 53s linear -31s infinite;
}
/* ── AURORA ORBITS (Josh 2026-08-18: "a full 360, both in terms of depth
 * and in terms of the x and y axes") ────────────────────────────────────
 *
 * Each blob travels a complete circle that passes THROUGH the depth axis —
 * toward the viewer and away again — rather than sliding around flat.
 *
 * The shape of each keyframe is `rotate(θ) translateZ(r) rotate(-θ)`, and
 * the trailing counter-rotation is the whole trick. Without it the blob is
 * a flat disc being spun in 3D, so it foreshortens to an ellipse and then
 * to NOTHING at 90° and 270° — a full 360 would make the light blink out
 * twice per lap. Counter-rotating keeps every blob square to the camera
 * (billboarded) while its POSITION sweeps the full circle, so the depth is
 * real but the glow never vanishes.
 *
 * Two orbit planes so the set tumbles on more than one axis: `…OrbitY`
 * sweeps left-right through depth, `…OrbitX` sweeps up-down through it.
 * `scale` rides along as a second depth cue, since perspective alone is
 * subtle on something this soft and this large.
 *
 * Periods are long (50–74s) and mutually prime-ish, with negative delays,
 * so the three never line up into an obvious synchronised pulse.
 */
@media (prefers-reduced-motion: reduce) {
  /* No orbit, and no counter-lift transition either — under reduced motion
     the lift itself is skipped in JS, so there is nothing to cancel. */
  .stl-how__aurora i { animation: none; }
  .stl-how__aurora { transition: none; }
}
@keyframes stlAuroraOrbitY {
  0%   { transform: rotateY(0deg)   translateZ(110px) rotateY(0deg)    scale(1); }
  25%  { transform: rotateY(90deg)  translateZ(110px) rotateY(-90deg)  scale(1.07); }
  50%  { transform: rotateY(180deg) translateZ(110px) rotateY(-180deg) scale(1); }
  75%  { transform: rotateY(270deg) translateZ(110px) rotateY(-270deg) scale(0.94); }
  100% { transform: rotateY(360deg) translateZ(110px) rotateY(-360deg) scale(1); }
}
@keyframes stlAuroraOrbitX {
  0%   { transform: rotateX(0deg)   translateZ(90px) rotateX(0deg)    scale(1); }
  25%  { transform: rotateX(90deg)  translateZ(90px) rotateX(-90deg)  scale(0.95); }
  50%  { transform: rotateX(180deg) translateZ(90px) rotateX(-180deg) scale(1); }
  75%  { transform: rotateX(270deg) translateZ(90px) rotateX(-270deg) scale(1.08); }
  100% { transform: rotateX(360deg) translateZ(90px) rotateX(-360deg) scale(1); }
}
/* Both axes at once, for the blob between the other two. */
@keyframes stlAuroraOrbitXY {
  0%   { transform: rotateY(0deg)   rotateX(0deg)   translateZ(100px) rotateX(0deg)    rotateY(0deg)    scale(1); }
  50%  { transform: rotateY(180deg) rotateX(180deg) translateZ(100px) rotateX(-180deg) rotateY(-180deg) scale(1.06); }
  100% { transform: rotateY(360deg) rotateX(360deg) translateZ(100px) rotateX(-360deg) rotateY(-360deg) scale(1); }
}
/* ── hero card ───────────────────────────────────────────────────────────
 *
 * The hero art used to hold .stl-mock: a static picture of a question card.
 * On 2026-08-18 it was replaced by the REAL live demo (the same markup and
 * IDs app.js's initHowDemo() drives), so the picture and all fifteen of its
 * rules are gone. What is left is the styling that makes the live card read
 * like that picture did.
 *
 * These numbers ARE .stl-mock's numbers — 400px wide, 24px padding, 20px
 * radius, 15.5px stem, 9px/12px choices with a 20px letter badge, 12.5px
 * signal pills, and its tilt and drift. They were copied deliberately rather
 * than re-derived, because the brief was "make it look like the original".
 *
 * The ONE thing not copied is a translucent fill. .stl-demo bases its
 * background on var(--stl-surface) = rgba(255,255,255,0.035); over the hero
 * aurora that reads as faded glass and the text loses most of its contrast.
 * .stl-mock used the opaque var(--stl-bg-2), and so does this. A translucent
 * fill is not a background — the same trap took a run at the Tailored tile
 * the day before.
 */
.stl-how__art .stl-demo {
      width: min(400px, 100%);
      max-width: none;
      margin: 0;
      padding: 24px;
      border: 1px solid var(--stl-hairline-hi);
      border-radius: 20px;
      background:
        linear-gradient(160deg, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.02) 55%, rgba(255, 255, 255, 0) 100%),
        var(--stl-bg-2);
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.08),
        0 30px 80px -20px rgba(0, 0, 0, 0.6);
      /* NO 3D TILT ON THIS CARD (Josh 2026-08-19, third report: "just make
         it so they stop changing in size"). It used to carry the mock's
         perspective(1400px) rotateY(-8deg) rotateX(3deg), and that tilt was
         the whole bug:
           a 3D-transformed, animated element is promoted to its own
           compositing layer, rasterised ONCE at a chosen scale and then
           projected. Every repaint of a child — hover background, a chip
           glow, a selected state — can force the layer to re-rasterise at
           its TRUE, unprojected scale, and the contents visibly jump size.
         Two previous fixes chased the triggers (remove the transform, then
         remove the background swap) and a third trigger appeared each time,
         because the triggers are unbounded: anything that repaints inside a
         projected layer does it. The projection is the only bounded thing,
         so it is what goes.
         This card is the INTERACTIVE demo now, not the decorative mock. A
         surface you click should not be skewed away from the pointer
         anyway. .stl-mock — the purely decorative twin — keeps its tilt.
         Do not reintroduce a 3D transform here without deleting the demo's
         interactivity first.

         The drift (translateY only, no projection) keeps the float. */
      animation: stlDemoFloat 7s ease-in-out infinite;
    }
    .stl-how__art .stl-demo:hover,
    .stl-how__art .stl-demo:focus-within { animation-play-state: paused; }
    @media (prefers-reduced-motion: reduce) {
      .stl-how__art .stl-demo { animation: none; }
    }

    .stl-how__art .stl-demo__tag  { margin: 0 0 12px; font-size: 11.5px; letter-spacing: 0.04em; }
    .stl-how__art .stl-demo__stem { margin: 0 0 14px; font-size: 15.5px; font-weight: 550; line-height: 1.45; }

    /* Four answers, two up — a 4-high stack would make the card a column. */
    .stl-how__art .stl-demo__choices {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 8px;
      margin: 0;
    }
    .stl-how__art .stl-choice {
      gap: 10px; margin: 0; padding: 9px 12px;
      border-radius: 10px; font-size: 13px; min-height: 0;
    }
    .stl-how__art .stl-choice__letter {
      width: 20px; height: 20px; border-radius: 6px; font-size: 11px; flex: 0 0 auto;
    }
    .stl-how__art .stl-choice__text { font-size: 13px; }

    .stl-how__art .stl-demo__ask {
      margin: 14px 0 8px; font-size: 10.5px; letter-spacing: 0.13em;
    }

    /* The signal pills as the mock drew them: dot INLINE with the label,
       centred, three across — and no colour word. The word belongs on the
       real quiz buttons where there is room; here it forced "Don't know"
       onto two lines and doubled the height of the row. */
    .stl-how__art .stl-signal {
      display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px;
    }
    .stl-how__art .stl-signal__btn {
      display: inline-flex; align-items: center; justify-content: center; gap: 7px;
      /* Sized to the HOVER rendering on purpose (Josh 2026-08-18: "I actually
         like the larger size"). The base .stl-signal__btn:hover applies
         translateY(-2px), and inside this card's perspective()/rotateY() 3D
         context that transform promotes the button to its own compositing
         layer, which re-rasterises WITHOUT the parent's projection — the text
         visibly popped larger/crisper on hover and snapped back on leave.
         Nothing in CSS changed a dimension; it was a rasterisation artefact.
         Fix: this larger size is now the permanent one, and hover applies no
         transform inside the card (below), so there is nothing to pop. */
      /* The bigger size is the ONLY size (Josh 2026-08-19). It used to be
         described as "sized to the hover rendering"; there is no hover
         rendering any more — the card is no longer projected, so what you
         see at rest is what the browser rasterises in every state. */
      padding: 11px 8px; border-radius: 10px; min-height: 0;
      font-size: 13.5px; font-weight: 600; text-align: center;
    }
    /* Hover feedback is back, and it is safe now (Josh 2026-08-19). The
       repaint-triggered re-rasterisation that ate the last two attempts
       came from the card's 3D projection; that projection is gone, so a
       repaint is just a repaint. Two standing constraints remain, and they
       are the whole reason this block exists rather than inheriting:
         1. NOTHING here may change a dimension — no padding, font-size,
            border-width or transform. Feedback is colour and glow only, so
            the row can never move or resize under the pointer.
         2. Each button glows in ITS OWN signal colour (currentColor is
            green/yellow/red per modifier), which is the feedback Josh was
            missing when hover was reduced to a border tint. */
    .stl-how__art .stl-signal__btn:hover {
      transform: none;
      background: var(--stl-surface-2);
      border-color: currentColor;
      /* No 0-blur ring here (Josh 2026-08-19, the "buttons change shape on
       * hover" report). The old `0 0 0 1px currentColor` painted flush
       * against the 1px border and read as the border jumping to 2px — the
       * exact illusion the 2026-08-07 tier decision retired. The diffuse
       * glow carries the colour feedback; glows are sanctioned, hard rings
       * on ANY state that isn't focus/error/drag are not. */
      box-shadow: 0 6px 20px -8px currentColor;
    }
    .stl-how__art .stl-signal__btn:hover .stl-signal__chip {
      box-shadow: 0 0 14px currentColor;
    }
    /* Answer hover is GREY here, not the accent (Josh 2026-08-18). Under the
       signal theme the accent is green, and green on this card is a VERDICT —
       the demo marks the correct answer green after you commit. A green flash
       on mere hover pre-announces the palette's strongest meaning. Neutral
       hover; the accent edge bar stays off too. Selection and the confidence
       buttons keep their existing treatment — only the four answers change. */
    .stl-how__art .stl-choice:hover {
      border-color: var(--stl-hairline-hi);
      background: var(--stl-surface-2);
    }
    .stl-how__art .stl-choice:hover::before { transform: scaleY(0); }
    .stl-how__art .stl-signal__sub { display: none; }
    /* NOWRAP IS LOAD-BEARING (Josh 2026-08-20, Safari: "big buttons until
       hover"). Measured in WebKit rather than guessed: at first paint the
       button is 78px wide and 60px tall; 2s later it is 111px wide and
       42px tall. The card starts narrower than its final 400px while the
       screen un-hides, "Don't know" wraps to two lines at that width, and
       the three buttons are grid items — so one wrapped label makes the
       whole row 18px taller.
       The layout corrects itself, but Safari does not repaint it: you get
       the tall row until a hover forces the repaint, which is exactly what
       Josh described. Chrome repaints and nobody ever saw it.
       So the fix is to make the row IMMUNE to the transient rather than to
       chase the repaint: a label that cannot wrap cannot change the row's
       height, so there is no wrong state left to paint. */
    .stl-how__art .stl-signal__lbl { font-size: 13.5px; white-space: nowrap; }

    /* nowrap has one cost: on a narrow phone the three columns get tight
       enough that "Don't know" would overflow its button. Paid for here,
       by shrinking the label and its padding instead of letting it wrap. */
    @media (max-width: 420px) {
      .stl-how__art .stl-signal__btn { padding-inline: 5px; gap: 5px; }
      .stl-how__art .stl-signal__lbl { font-size: 12px; }
    }
    .stl-how__art .stl-signal__chip {
      width: 8px; height: 8px; flex: 0 0 auto; box-shadow: none;
    }

    /* The verdict panel at the same scale, so answering does not turn the
       compact card into a page. */
    .stl-how__art .stl-demo__result  { margin-top: 14px; }
    .stl-how__art .stl-demo__chip    { font-size: 11.5px; padding: 5px 10px; }
    .stl-how__art .stl-demo__verdict { font-size: 12.5px; line-height: 1.5; }
    .stl-how__art .stl-demo__expl,
    .stl-how__art .stl-demo__next    { font-size: 11.5px; line-height: 1.5; }
    .stl-how__art .stl-demo__actions .stl-btn {
      /* Both CTAs are pills at the same height (Josh 2026-08-18). The
         gradient one is border-only, so its 1.5px border has to be matched
         by the neutral one or they sit half a pixel apart. */
      font-size: 12px; padding: 8px 14px; min-height: 0;
      border-radius: var(--r-pill);
      white-space: nowrap;
    }


    /* The floating verdict pills, repositioned for a 400px card.
       Their original offsets were tuned against a mock that filled more of
       the column; at this size the red one landed on top of the question
       stem. Nudged outward so they hang off the card's edges the way they
       did originally, rather than over its text. */
    /* Offsets are relative to .stl-how__art (532x440); the card is 400x292
       centred in it, so its top edge sits at about 17% and its right edge at
       about 88%. Misconception is pinned just above that top edge on the
       right so it straddles the card's top-right corner (Josh 2026-08-18).
       Mastery comes down off the very top, Lucky comes up off the bottom. */
    /* Tuned again 2026-08-18 (Josh): Misconception up to graze the card's
       top-right corner, Lucky up off the bottom edge, Mastery down so the
       three read as a loose ring around the card rather than a column. */
    .stl-how__art .stl-float--g { top: 9%;      left: -7%; }
    .stl-how__art .stl-float--r { top: 6%;      right: -3%; }
    .stl-how__art .stl-float--y { bottom: 12%;  right: -2%; }

    /* "Try it out live" — an annotation, deliberately NOT a fourth pill.
       The three pills name OUTCOMES the demo can produce; this is an
       instruction, so it wears the site's annotation grammar instead:
       mono eyebrow caps + a hand-drawn arrow curving into the card. No
       border, no fill, no drift — it points, it does not float. Hidden
       along with the pills once the demo has answered, because "try it"
       is noise beside a result the visitor just produced. */
    .stl-how__art .stl-float--try {
      position: absolute;
      /* Lower and further right (Josh 2026-08-18) — tucks under the
         card's lower-left region instead of hanging off mid-air. */
      left: 7%; bottom: 5%;
      display: flex; align-items: flex-end; gap: 8px;
      /* Handwritten annotation (Josh 2026-08-18): Caveat, subset-loaded
         in index.html to just these glyphs (~3KB). Mixed case — caps
         defeat the handwriting. Falls back to cursive, then the mono
         eyebrow look, if the subset ever fails to load. */
      font-family: "Caveat", cursive, var(--stl-font-mono);
      font-size: 21px; font-weight: 600;
      letter-spacing: 0.01em;
      color: var(--stl-text-muted);
      transform: rotate(-4deg);
      pointer-events: none;
    }
    .stl-how__art .stl-float--try svg {
      width: 34px; height: 30px; flex: 0 0 auto;
      color: var(--stl-text-subtle);
      /* points up-right into the card's lower-left corner */
      transform: translateY(2px) rotate(6deg);
    }
    .stl-how__art:has(#how-demo-result:not([hidden])) .stl-float--try {
      opacity: 0; visibility: hidden;
      transition: opacity .28s ease, visibility .28s;
    }
    .stl-how__art .stl-float--try { transition: opacity .28s ease, visibility .28s; }
    @media (max-width: 640px) {
      /* CORRECTION 2026-08-18: an earlier version of this comment claimed the
         art column "does not render on phones". It does — measured at every
         width down to 375px, .stl-how__art is display:flex throughout. The
         annotation is hidden here because it is a pointing gesture that needs
         room beside the card, not because the card is absent. */
      .stl-how__art .stl-float--try { display: none; }
    }

    /* ...and they retire once the demo has actually answered.
       They are static labels naming the six outcomes, which was fine beside
       a mock. Beside a LIVE demo they collide with the thing they describe:
       answering with a confident wrong pick rendered the real verdict chip
       "Misconception · sure + wrong" directly beneath a floating pill saying
       exactly the same words, which reads as a duplicate or, worse, as a
       second opinion. So they introduce what the card can tell you, then get
       out of the way once it has told you. */
    .stl-how__art:has(#how-demo-result:not([hidden])) .stl-float {
      opacity: 0;
      visibility: hidden;
      transition: opacity .28s ease, visibility .28s;
    }
    .stl-how__art .stl-float { transition: opacity .28s ease, visibility .28s; }

    /* No rainbow strip across the top edge. .stl-demo::before paints a 2px
       var(--stl-rainbow) bar there; that belongs to the live-demo component
       as it sat mid-page, and the .stl-mock this card replaces never had one.
       It was the last visible difference between the two (Josh 2026-08-18). */
    .stl-how__art .stl-demo::before { content: none; }
    @media (max-width: 640px) {
      .stl-how__art .stl-demo { width: 100%; padding: 18px; transform: none; animation: none; }
    }

/* Flat drift for the interactive demo card — translate only, no
 * perspective, so the card never becomes a projected raster. See the
 * .stl-how__art .stl-demo rule for the full reasoning. */
@keyframes stlDemoFloat {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-10px); }
}

@keyframes stlMockFloat {
  0%, 100% { transform: perspective(1400px) rotateY(-8deg) rotateX(3deg) translateY(0); }
  50%      { transform: perspective(1400px) rotateY(-8deg) rotateX(3deg) translateY(-10px); }
}

.stl-float {
  position: absolute;
  padding: 8px 14px;
  border: 1px solid;
  border-radius: var(--r-pill);
  background: var(--stl-bg-2);
  box-shadow: 0 12px 30px -12px rgba(0, 0, 0, 0.6);
  font-size: 12.5px; font-weight: 600;
  white-space: nowrap;
}
.stl-float--g { top: 4%;    left: -2%; color: var(--stl-green-text);  border-color: rgba(var(--stl-green-rgb), 0.45);  animation: stlFloatDrift 6s ease-in-out infinite; }
.stl-float--y { bottom: -2%; right: 4%; color: var(--stl-yellow-text); border-color: rgba(var(--stl-yellow-rgb), 0.45);  animation: stlFloatDrift 7.5s ease-in-out -2s infinite; }
.stl-float--r { top: 32%;   right: 0;  color: var(--stl-red-text);    border-color: rgba(var(--stl-red-rgb), 0.45); animation: stlFloatDrift 8.5s ease-in-out -4s infinite; }
@keyframes stlFloatDrift {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-9px); }
}

/* ---- shared block scaffolding: centered heads, roomy bands ---- */
.stl-how__block { margin-top: clamp(56px, 8vw, 96px); }
.stl-how__kicker {
  margin: 0 0 10px;
  font-family: var(--stl-font-mono);
  font-size: 11.5px; font-weight: 500;
  letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--stl-accent);
  text-align: center;
}
.stl-how__h2 {
  margin: 0 auto 12px;
  max-width: 26ch;
  font-family: var(--stl-font-display);
  font-size: clamp(25px, 3vw, 36px);
  font-weight: 650; letter-spacing: -0.02em; line-height: 1.15;
  color: var(--stl-text);
  text-align: center;
}
/* Double-class selector on purpose: the base .stl-how__p rule sits
 * LATER in the sheet and its `margin: 0 0 12px` shorthand would win
 * the cascade at equal specificity, killing the auto-centering
 * (Josh 2026-08-10 — the "misaligned subtitles" bug). */
.stl-how__p.stl-how__p--c { text-align: center; margin-left: auto; margin-right: auto; max-width: 58ch; }

/* ---- problem twins ---- */
.stl-how__twins { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; max-width: 116ch; margin: 28px auto 0; }
.stl-twin {
  margin: 0; padding: 20px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background: var(--stl-surface);
}
.stl-twin__id { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.stl-twin__ava {
  width: 56px; height: 56px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid var(--stl-hairline-hi);
  flex: 0 0 auto;
}
.stl-twin__name {
  font-family: var(--stl-font-mono);
  font-size: 12px; letter-spacing: 0.08em;
  color: var(--stl-text-subtle);
}
.stl-twin__score {
  font-family: var(--stl-font-display);
  font-size: 42px; font-weight: 700; letter-spacing: -0.02em; line-height: 1;
  color: var(--stl-text);
}
.stl-twin__score span { font-size: 17px; font-weight: 500; color: var(--stl-text-subtle); margin-left: 2px; }
.stl-twin__score em {
  font-style: normal;
  font-family: "Geist", -apple-system, system-ui, sans-serif;
  font-size: 12.5px; font-weight: 500; letter-spacing: 0;
  color: var(--stl-text-subtle);
  margin-left: 8px;
}
.stl-twin__ticks { display: flex; gap: 5px; margin: 14px 0 12px; flex-wrap: wrap; }
.stl-twin__ticks i { width: 15px; height: 15px; border-radius: 5px; border: 2px solid transparent; }
.stl-twin__ticks i[data-t="g1"] { background: var(--stl-green); }
.stl-twin__ticks i[data-t="y1"] { background: var(--stl-yellow); }
.stl-twin__ticks i[data-t="r1"] { background: var(--stl-red); }
.stl-twin__ticks i[data-t="g0"] { border-color: var(--stl-green); }
.stl-twin__ticks i[data-t="y0"] { border-color: var(--stl-yellow); }
.stl-twin__ticks i[data-t="r0"] { border-color: var(--stl-red); }
.is-rev .stl-twin__ticks i { animation: stlTickIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; }
.is-rev .stl-twin__ticks i:nth-child(1)  { animation-delay: 0.35s; }
.is-rev .stl-twin__ticks i:nth-child(2)  { animation-delay: 0.42s; }
.is-rev .stl-twin__ticks i:nth-child(3)  { animation-delay: 0.49s; }
.is-rev .stl-twin__ticks i:nth-child(4)  { animation-delay: 0.56s; }
.is-rev .stl-twin__ticks i:nth-child(5)  { animation-delay: 0.63s; }
.is-rev .stl-twin__ticks i:nth-child(6)  { animation-delay: 0.70s; }
.is-rev .stl-twin__ticks i:nth-child(7)  { animation-delay: 0.77s; }
.is-rev .stl-twin__ticks i:nth-child(8)  { animation-delay: 0.84s; }
.is-rev .stl-twin__ticks i:nth-child(9)  { animation-delay: 0.91s; }
.is-rev .stl-twin__ticks i:nth-child(10) { animation-delay: 0.98s; }
@keyframes stlTickIn {
  from { opacity: 0; transform: scale(0.4); }
  60%  { opacity: 1; transform: scale(1.15); }
  to   { opacity: 1; transform: scale(1); }
}
.stl-twin__cap { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--stl-text-muted); }
/* Caption outcome highlights (Josh 2026-08-11): bold + signal color. */
.stl-twin__hl { font-weight: 600; }
.stl-twin__hl--g { color: var(--stl-green-text); }
.stl-twin__hl--y { color: var(--stl-yellow-text); }
.stl-twin__hl--r { color: var(--stl-red-text); }
.stl-how__twins-legend { margin: 14px auto 0; max-width: 60ch; text-align: center; font-size: 12.5px; color: var(--stl-text-subtle); }

/* ---- live demo ---- */
/* Demo picks are neutral gray too (Josh 2026-08-11) — same reasoning
 * as the hero mock; the confidence row carries the color. */
.stl-demo .stl-choice.is-selected {
  border-color: rgba(148, 163, 184, 0.55);
  background: rgba(148, 163, 184, 0.10);
}
.stl-demo .stl-choice.is-selected .stl-choice__letter {
  background: #94a3b8; border-color: #94a3b8; color: var(--stl-bg-deep);
}
.stl-demo .stl-choice::before { background: #94a3b8; }
.stl-demo {
  max-width: 740px;
  margin: 28px auto 0;
  padding: clamp(20px, 3vw, 32px);
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-lg);
  background:
    linear-gradient(165deg, rgba(148, 163, 184, 0.05) 0%, rgba(255, 255, 255, 0.02) 50%, transparent 80%),
    var(--stl-surface);
  box-shadow: 0 18px 50px -16px rgba(0, 0, 0, 0.4);
  position: relative;
  overflow: hidden;
}
.stl-demo::before {
  content: "";
  position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: var(--stl-rainbow);
  opacity: 0.8;
}
.stl-demo__tag {
  display: flex; align-items: center; gap: 9px;
  margin: 0 0 14px;
  font-family: var(--stl-font-mono);
  font-size: 12px; letter-spacing: 0.05em;
  color: var(--stl-text-subtle);
}
.stl-demo__stem { margin: 0 0 18px; font-size: clamp(17px, 1.6vw, 19px); font-weight: 550; line-height: 1.45; color: var(--stl-text); }
.stl-demo__choices { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
/* The quiz's .stl-choice is a <label> and inherits the app color; these
 * are <button>s, which default to UA black — so the color has to be
 * explicit or the numbers vanish into the dark card (Josh 2026-08-10). */
.stl-demo__choices .stl-choice {
  text-align: left;
  color: var(--stl-text);
  font: inherit;
  cursor: pointer;
}
.stl-demo__choices .stl-choice .stl-choice__letter { color: var(--stl-text-muted); }
.stl-demo__choices .stl-choice.is-selected .stl-choice__letter { color: var(--stl-bg-deep); }
.stl-demo__choices .stl-choice:disabled { cursor: default; }
.stl-demo__choices .stl-choice:disabled:not(.is-demo-correct):not(.is-demo-wrong) { opacity: 0.5; }
.stl-demo__choices .is-demo-correct { border-color: var(--stl-green); background: rgba(var(--stl-green-rgb), 0.08); }
.stl-demo__choices .is-demo-correct .stl-choice__letter { background: var(--stl-green); border-color: var(--stl-green); color: #06240f; }
.stl-demo__choices .is-demo-wrong { border-color: var(--stl-red); background: rgba(var(--stl-red-rgb), 0.07); }
.stl-demo__choices .is-demo-wrong .stl-choice__letter { background: var(--stl-red); border-color: var(--stl-red); color: #2b060d; }
.stl-demo__signal { margin-top: 22px; }
/* The ask line, in the same mono-label voice as the hero mock's "HOW
 * SURE ARE YOU?" — the quiz's .stl-quiz__signal-help was designed for
 * the quiz footer and tracked out wrong in this card (Josh 2026-08-10). */
.stl-demo__ask {
  margin: 0 0 10px;
  font-family: var(--stl-font-mono);
  font-size: 11px; letter-spacing: 0.11em; text-transform: uppercase;
  color: var(--stl-text-subtle);
  /* Crossfade for text swaps (Josh 2026-08-18: the instant swap to the
   * armed line was jarring). JS adds .is-swapping, waits 140ms, swaps
   * the text, removes the class. */
  transition: opacity 140ms var(--ease-out);
}
.stl-demo__ask.is-swapping { opacity: 0; }
/* Not-yet-armed buttons sit at a readable dim, not the app's 0.4
 * disabled ghost — on a marketing page they still have to say
 * "green / yellow / red" from across the room. */
.stl-demo__signal .stl-signal__btn:disabled { opacity: 0.55; }
/* Armed pick stays lit while its siblings dim after the verdict. */
.stl-demo__signal .stl-signal__btn.is-demo-sigpick:disabled {
  opacity: 1;
  box-shadow: 0 0 22px -8px currentColor;
}
/* The container-level `animation: stlDemoRise .45s both` was removed
 * 2026-08-18: the reveal now interpolates HEIGHT on this element while its
 * children stagger in, and a simultaneous translate+fade on the same box
 * fought both — it also meant the box was mid-animation at the moment
 * expandResult() measures scrollHeight. One motion per element. */
.stl-demo__result { margin-top: 20px; }
.stl-demo__result[hidden] { display: none; }
.stl-demo__chip {
  display: inline-flex; align-items: center;
  margin: 0 0 12px;
  padding: 7px 15px;
  border: 1px solid;
  border-radius: var(--r-pill);
  font-weight: 650; font-size: 14px; letter-spacing: 0.01em;
  animation: stlChipSpring 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes stlChipSpring {
  from { opacity: 0; transform: scale(0.55); }
  to   { opacity: 1; transform: scale(1); }
}
.stl-demo__chip[data-state="mastery"]       { color: var(--stl-green-text);  border-color: var(--stl-green);  background: rgba(var(--stl-green-rgb), 0.10); }
.stl-demo__chip[data-state="lucky"],
.stl-demo__chip[data-state="gap"]           { color: var(--stl-yellow-text); border-color: var(--stl-yellow); background: rgba(var(--stl-yellow-rgb), 0.10); }
.stl-demo__chip[data-state="misconception"],
.stl-demo__chip[data-state="fluke"]         { color: var(--stl-red-text);    border-color: var(--stl-red);    background: rgba(var(--stl-red-rgb), 0.10); }
.stl-demo__chip[data-state="cold"]          { color: var(--stl-text-muted);  border-color: var(--stl-hairline-hi); background: var(--stl-surface-2); }
.stl-demo__verdict { margin: 0 0 12px; font-size: 15px; line-height: 1.6; color: var(--stl-text); max-width: 60ch; }
/* The verdict sentence carries the outcome's colour (Josh 2026-08-18:
 * "this text should reflect the colour of whether you got it right, not
 * sure, or didn't know") — same mapping as the chip above, readable
 * -text pastels rather than the saturated mid-tones. Cold stays muted:
 * "no idea" has no colour to celebrate. */
.stl-demo__verdict[data-state="mastery"]       { color: var(--stl-green-text); }
.stl-demo__verdict[data-state="lucky"],
.stl-demo__verdict[data-state="gap"]           { color: var(--stl-yellow-text); }
.stl-demo__verdict[data-state="misconception"],
.stl-demo__verdict[data-state="fluke"]         { color: var(--stl-red-text); }
.stl-demo__verdict[data-state="cold"]          { color: var(--stl-text-muted); }
.stl-demo__expl {
  margin: 0 0 12px;
  padding: 13px 15px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  background: var(--stl-surface-2);
  font-size: 14px; line-height: 1.6; color: var(--stl-text-muted);
  max-width: 60ch;
}
.stl-demo__expl b { color: var(--stl-text); font-weight: 600; }

/* ── THE REVEAL FLOATS (desktop only) ──────────────────────────────────
 *
 * Josh 2026-08-18: the page should not grow. The verdict panel expands as an
 * absolutely-positioned sheet hanging off the card's bottom edge, overlaying
 * the section beneath, so document height never changes on desktop.
 *
 * WHY 981px: the hero is two-column above 980px and stacks below it
 * (`@media (max-width: 980px)`). Once stacked, the card is the page's main
 * column and a floating sheet would cover the content a phone user is
 * about to scroll to — so below this width it stays IN FLOW and the page
 * grows, which is what Josh predicted. Verified: .stl-how__art renders at
 * every width down to 375px, so there is no "mobile doesn't show it" escape
 * hatch. (An earlier comment of mine claimed exactly that. It was wrong.)
 *
 * THE FOUR THINGS THAT MADE THIS NON-TRIVIAL, all measured rather than
 * assumed:
 *
 *  1. `.stl-demo` sets `overflow: hidden`, so an absolute child is CLIPPED
 *     to the collapsed card. Hence `.is-demo-open { overflow: visible }`.
 *     Safe here specifically because the hero card kills the pseudo the
 *     clip protects — `.stl-how__art .stl-demo::before { content: none }` —
 *     so nothing else depends on it. Do NOT lift the clip on the base card.
 *  2. `.stl-demo` carries a 3D transform, which makes it the containing
 *     block. `top: 100%` therefore resolves against its PADDING box, whose
 *     bottom edge is the inner edge of the border — exactly the card's
 *     bottom. `left/right: -1px` covers the 1px border so the sheet's edges
 *     line up with the card's. The transform also means the sheet inherits
 *     the card's rotateY projection, which is what makes it read as the
 *     card growing rather than a separate object.
 *  3. PAINT ORDER. `.stl-how__hero` has a transform, so it is a stacking
 *     context with `z-index: auto` — the sections after it paint on top no
 *     matter what z-index the sheet gets. The hero itself has to be lifted,
 *     and only while open, so nothing else about the page changes when it
 *     is not: `.stl-how__hero.is-demo-open`.
 *  4. It floats over real text, so it must be genuinely opaque —
 *     `--stl-panel` + a blur, the rule established when the user menu was
 *     found translucent. NOTE for Safari: this stacks backdrop-filter and
 *     box-shadow inside a 3D transform, the combination that clips shadows
 *     in Safari. Worth a look on a real Mac; the fallback is dropping the
 *     blur for a fully opaque fill.
 *
 * SHORT VIEWPORTS: the sheet is capped and scrolls internally rather than
 * running off the end of the document, and expandResult() clamps its
 * measured target to this same cap so the animation cannot aim past it.
 */
@media (min-width: 981px) {
  /* ONE OBJECT, NOT TWO (Josh 2026-08-18: "weird and unattached").
   * The sheet must read as the card continuing, so every edge between them
   * is removed rather than merely matched:
   *   - the card drops its bottom border and bottom radii while open, and
   *     `top: 100%` lands exactly where that border was, so there is no
   *     hairline across the middle and no gap;
   *   - the fill is `--stl-bg-2`, NOT `--stl-panel`. The card's own
   *     background is a 160deg gradient that reaches full transparency at
   *     100%, so the card's bottom edge already IS flat --stl-bg-2 — the
   *     sheet picking the same token makes the seam invisible, where the
   *     panel token would have been a visibly different tone butted against
   *     it. --stl-bg-2 is opaque in both themes, so this also satisfies the
   *     "floating surfaces must not be see-through" rule without a
   *     backdrop-filter, which is a bonus: blur + box-shadow inside a 3D
   *     transform is the combination that clips shadows in Safari.
   *   - the card's big drop shadow moves TO the sheet. Left on the card it
   *     was casting down onto the sheet's top edge, drawing the exact
   *     dividing line this is trying to erase. */
  .stl-how__art .stl-demo.is-demo-open {
    overflow: visible;
    border-bottom: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  }
  .stl-how__art .stl-demo.is-demo-open .stl-demo__result {
    position: absolute;
    top: 100%;
    left: -1px;
    right: -1px;
    margin-top: 0;
    z-index: 3;
    padding: 20px 24px 24px;
    background: var(--stl-bg-2);
    border: 1px solid var(--stl-hairline-hi);
    border-top: 0;
    border-radius: 0 0 20px 20px;
    box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.6);
    max-height: min(560px, 74vh);
    overflow-y: auto;
  }
  /* While the height animates, JS owns overflow (hidden, so the box can be
     measured and clipped mid-grow); the scroll cap applies once released. */
  .stl-how__art .stl-demo.is-demo-open .stl-demo__result.is-expanding {
    overflow-y: hidden;
  }

  /* THE LIFT. Because the page no longer grows, the sheet would hang down
   * over the section below. Raising the whole art column as the sheet opens
   * buys back most of that overlap and keeps the combined card+sheet
   * optically centred in the space the hero already reserved.
   *
   * It is applied to .stl-how__art, NOT the card: the card's transform is
   * owned by the infinite stlMockFloat drift, and anything set here would
   * either be overridden by the animation or would have to kill it — and
   * killing it mid-cycle jumps, because only `animation-play-state: paused`
   * holds position. Lifting the parent composes cleanly with the drift and
   * carries the sheet, the aurora and the card as one piece.
   *
   * The distance is computed in JS from the sheet's measured height AND the
   * room actually available above the card, so it can never slide up under
   * the sticky navbar. */
  .stl-how__art {
    transition: transform 460ms cubic-bezier(0.22, 0.68, 0.24, 1);
  }
  .stl-how__art.is-demo-lifted {
    transform: translateY(calc(-1 * var(--demo-lift, 0px)));
  }
  /* While the verdict is open, the whole art column slides LEFT to make
   * room for the radar panel at its right (2026-08-25). Composed with
   * the lift in one rule — two rules would fight over `transform` — and
   * keyed on is-demo-open, not is-demo-lifted, because the lift can be
   * zero (card near the navbar) while the panel still needs its room. */
  .stl-how__hero.is-demo-open .stl-how__art {
    transform: translateX(-150px) translateY(calc(-1 * var(--demo-lift, 0px)));
  }
  /* Narrow desktops: the copy column cannot spare 118px, and the page
   * cannot spare the panel's full overhang — everything pulls in. */
  @media (max-width: 1199px) {
    .stl-how__hero.is-demo-open .stl-how__art {
      transform: translateX(-96px) translateY(calc(-1 * var(--demo-lift, 0px)));
    }
    .stl-demo__radarpanel { width: 224px; right: -44px; }
  }

  .stl-how__hero.is-demo-open { position: relative; z-index: 5; }
}

/* ── THE RADAR PANEL ───────────────────────────────────────────────────
 * (Josh 2026-08-25.) Answer the demo and the mastery radar swings in
 * beside the verdict — its own boxed panel, tilted in 3D one way while
 * the card leans the other.
 *
 * The tilt split is the whole design, and it is §5.7 compliance, not
 * taste:
 *   - The PANEL gets the real 3D projection (perspective + rotateY).
 *     It is decorative — aria-hidden, pointer-events none, nothing to
 *     hover — so no repaint can ever force the projected layer to
 *     re-rasterise mid-look. Projection on decoration is sanctioned;
 *     that is exactly what .stl-mock does.
 *   - The CARD's counter-tilt is a 2D rotate baked into its float
 *     keyframes. The card holds the two CTAs, and 2026-08-19 taught us
 *     (three bug reports) that an interactive surface inside a 3D
 *     projection re-rasterises on any child repaint and visibly jumps.
 *     2D rotate rasterises at true scale, so a hover repaint redraws
 *     identically. Do not "upgrade" the card to perspective().
 * Desktop-only (≥981px), the same gate as the floating verdict sheet —
 * below it the sheet is in-flow and the phone pass (2026-08-25) keeps
 * the art column to exactly one card. */
.stl-demo__radarpanel {
  /* IN FRONT of the card (Josh 2026-08-25, option F of the see-more
   * study): flipping the stack is the only move that shows 100% of the
   * analysis. What it covers on the card is the answered B/D column —
   * spent content in the verdict state — and pointer-events: none below
   * means every click still lands on the card. The shadow falls LEFT,
   * onto the card, because that is where the light says it should. */
  position: absolute; z-index: 3;
  width: 250px; right: -92px; top: 50%;
  border: 1px solid var(--stl-hairline-hi);
  border-radius: 16px;
  padding: 14px 14px 11px;
  background:
    linear-gradient(160deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02) 55%, transparent),
    var(--stl-bg-2);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.07),
    -24px 30px 70px -18px rgba(0, 0, 0, 0.75);
  pointer-events: none;
  display: grid; gap: 6px;
}
.stl-demo__radarpanel .stl-hana__radar { width: 100%; height: auto; color: var(--stl-text-subtle); }
.stl-demo__radarnote {
  margin: 0; padding-top: 8px;
  border-top: 1px solid var(--stl-hairline);
  font-size: 11px; line-height: 1.5; color: var(--stl-text-subtle);
}
@media (max-width: 980px) {
  .stl-demo__radarpanel { display: none !important; }
}
@media (min-width: 981px) {
  .stl-demo__radarpanel {
    opacity: 0; visibility: hidden;
    transform: translateY(-33%) translateX(30px);
    transition:
      opacity 0.45s var(--ease-out),
      transform 0.6s cubic-bezier(0.22, 0.68, 0.24, 1),
      visibility 0s 0.45s;
  }
  .stl-how__hero.is-demo-open .stl-demo__radarpanel {
    opacity: 1; visibility: visible;
    /* Pure depth turn — no flat rotate() lean. -33%, not -50%: Josh
     * called the first placement "a little bit too high"; the panel now
     * hangs at the card's midline rather than beside the question. */
    transform: translateY(-33%) perspective(900px) rotateY(-13deg) rotateX(2deg);
    transition-delay: 0.3s, 0.3s, 0.3s;
  }
  /* The card takes the OPPOSITE 3D turn while the verdict is open
   * (Josh 2026-08-25: "so they are sort of almost facing each other").
   * §5.7 caveat, stated honestly: this is a projected layer holding two
   * CTAs, which is the shape of the 2026-08-19 bug. What makes it
   * tolerable here is that the FLOAT ANIMATION IS OFF in this state —
   * the measured artefact was projection + animation + repaint, where
   * the raster scale changed frame to frame. A static projection
   * re-rasters at one scale, and the CTAs' hover is colour/glow only.
   * If the buttons ever visibly pop on hover in this state, the fix is
   * to remove THIS transform, not to re-pin their sizes.
   *
   * is-demo-closing is the EXIT half (Josh 2026-08-25: "there needs to
   * be a transition out effect, the same as the transition in"): it
   * keeps the float off and the transition live while the tilt returns
   * to none, so the card un-turns instead of snapping the moment the
   * open class drops. JS holds it for 620ms, then the float restarts
   * from frame 0 — translateY(0), exactly where the un-tilt ends. */
  .stl-how__hero.is-demo-open .stl-how__art .stl-demo,
  .stl-how__hero.is-demo-closing .stl-how__art .stl-demo {
    z-index: 2;
    animation: none;
    transition: transform 0.55s cubic-bezier(0.22, 0.68, 0.24, 1);
  }
  .stl-how__hero.is-demo-open .stl-how__art .stl-demo {
    transform: perspective(1100px) rotateY(5deg) rotateX(1deg);
  }
}
/* The polygon draws itself once the panel is on screen; the vertex dots
 * pop after it, travelling the ring. Same choreography as the hana card,
 * scoped here because that one keys off .stl-how__block.is-rev and this
 * panel lives in the hero. */
.stl-demo__rpoly { stroke-dasharray: 620; stroke-dashoffset: 620; fill-opacity: 0; }
.stl-how__hero.is-demo-open .stl-demo__rpoly {
  stroke-dashoffset: 0; fill-opacity: 0.14;
  transition: stroke-dashoffset 0.9s 0.5s var(--ease-out), fill-opacity 0.4s 1.2s var(--ease-out);
}
.stl-demo__rdot { opacity: 0; transform: scale(0); transform-box: fill-box; transform-origin: center; }
.stl-how__hero.is-demo-open .stl-demo__rdot { animation: howTickPop 0.3s var(--ease-spring) forwards; }
.stl-how__hero.is-demo-open .stl-demo__rdot:nth-of-type(1) { animation-delay: 1.00s; }
.stl-how__hero.is-demo-open .stl-demo__rdot:nth-of-type(2) { animation-delay: 1.08s; }
.stl-how__hero.is-demo-open .stl-demo__rdot:nth-of-type(3) { animation-delay: 1.16s; }
.stl-how__hero.is-demo-open .stl-demo__rdot:nth-of-type(4) { animation-delay: 1.24s; }
.stl-how__hero.is-demo-open .stl-demo__rdot:nth-of-type(5) { animation-delay: 1.32s; }
.stl-how__hero.is-demo-open .stl-demo__rdot:nth-of-type(6) { animation-delay: 1.40s; }

/* ── THE REVEAL ────────────────────────────────────────────────────────
 * Josh 2026-08-18: "a beautiful transition ... the container should expand
 * animated". JS owns the height numbers (see initHowDemo's expandResult);
 * CSS owns the easing and the stagger.
 *
 * The curve matters more than the duration here. A standard ease-out
 * arrives and stops, which on a growing box reads mechanical. This
 * overshoot-free "settle" curve decelerates late, so the card looks like it
 * comes to rest rather than halting — the same instinct as --ease-spring,
 * without the bounce that would fight the text rising inside it.
 */
.stl-demo__result.is-expanding {
  transition: height 420ms cubic-bezier(0.22, 0.68, 0.24, 1);
  will-change: height;
}
/* Children start displaced and rise in sequence. `> *` rather than named
 * classes so adding a paragraph to the result cannot silently opt out of
 * the choreography. */
.stl-demo__result > * {
  opacity: 0;
  transform: translateY(9px);
  transition: opacity 340ms var(--ease-out), transform 340ms var(--ease-out);
}
.stl-demo__result.is-revealed > * { opacity: 1; transform: none; }
.stl-demo__result.is-revealed > *:nth-child(1) { transition-delay: 0ms; }
.stl-demo__result.is-revealed > *:nth-child(2) { transition-delay: 70ms; }
.stl-demo__result.is-revealed > *:nth-child(3) { transition-delay: 140ms; }
.stl-demo__result.is-revealed > *:nth-child(4) { transition-delay: 210ms; }
.stl-demo__result.is-revealed > *:nth-child(5) { transition-delay: 280ms; }

/* beat 0 — the committed signal button holds its colour while the two
 * not-chosen fade back, so the row answers "which did I say" at a glance
 * for the whole time the verdict is on screen. */
/* NOTE: no sibling-dim rule here on purpose. `.stl-demo__signal
 * .stl-signal__btn:disabled` (0.55) and `.is-demo-sigpick:disabled` (1)
 * already dim the not-chosen buttons the moment the row disables — I wrote a
 * second one keyed to .is-demo-committed and it was pure duplication, which
 * is the exact failure mode Rule #4 names. .is-demo-committed now carries
 * ONLY the pulse below. What it added: a transition, so the dim eases in
 * rather than snapping. */
.stl-demo__signal .stl-signal__btn { transition: opacity 260ms var(--ease-out), transform var(--dur-fast) var(--ease-out), border-color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out); }
.stl-demo__signal.is-demo-committed .stl-signal__btn.is-demo-sigpick {
  animation: stlDemoCommit 340ms var(--ease-out);
}
@keyframes stlDemoCommit {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.045); }
  100% { transform: scale(1); }
}

/* beat 1 — adjudication lands with a short glow rather than an instant
 * repaint, so the eye is drawn to the marked answers before the verdict
 * text explains them. */
.stl-demo .stl-choice.is-demo-correct { animation: stlDemoMark 420ms var(--ease-out); }
.stl-demo .stl-choice.is-demo-wrong   { animation: stlDemoMark 420ms var(--ease-out); }
@keyframes stlDemoMark {
  0%   { transform: translateX(0); }
  30%  { transform: translateX(-1.5px); }
  60%  { transform: translateX(1px); }
  100% { transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {
  /* Reduced motion gets the same information with no movement: JS skips the
     height animation and adds .is-revealed immediately, and these zero out
     the displacement and the accents. */
  .stl-demo__result.is-expanding { transition: none; }
  .stl-how__art { transition: none; }
  .stl-demo__result > * { opacity: 1; transform: none; transition: none; }
  .stl-demo__result.is-revealed > * { transition-delay: 0ms !important; }
  .stl-demo__signal.is-demo-committed .stl-signal__btn.is-demo-sigpick,
  .stl-demo .stl-choice.is-demo-correct,
  .stl-demo .stl-choice.is-demo-wrong { animation: none; }
  /* The radar panel: same composition, no choreography. */
  .stl-demo__radarpanel { transition: opacity 0.45s var(--ease-out), visibility 0s 0.45s; }
  .stl-demo__rpoly { stroke-dasharray: none; stroke-dashoffset: 0; fill-opacity: 0.14; transition: none; }
  .stl-demo__rdot { opacity: 1; transform: none; animation: none !important; }
}
.stl-demo__actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-2);
  align-items: stretch;
  margin-top: 2px;
}
.stl-demo__actions .stl-btn { justify-content: center; }

.stl-demo__next { margin: 0 0 14px; font-size: 13.5px; color: var(--stl-text-subtle); max-width: 60ch; }

/* ---- three steps ---- */
.stl-how__steps { list-style: none; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; margin: 28px auto 0; padding: 0; }
.stl-step {
  padding: 26px 24px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background: var(--stl-surface);
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.35s ease;
}
.stl-step:hover { transform: translateY(-4px); border-color: var(--stl-hairline-hi); }
.stl-step__n {
  display: block;
  margin-bottom: 14px;
  font-family: var(--stl-font-mono);
  font-size: 15px; font-weight: 600; letter-spacing: 0.12em;
  background: var(--stl-rainbow);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* ---- step micro-UIs: one extrapolated screen per callout ---- */
.stl-step__viz {
  height: 112px;
  margin: 0 0 16px;
  padding: 14px;
  display: flex; align-items: center; justify-content: center; gap: 14px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  background: var(--stl-surface-2);
  overflow: hidden;
  color: var(--stl-text-subtle); /* currentColor for the svg webs/axes */
}
.stl-sviz-q { display: grid; gap: 7px; min-width: 0; }
.stl-sviz-q__row {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px;
  border: 1px solid var(--stl-hairline);
  border-radius: 8px;
  background: var(--stl-surface);
  font-size: 12px; color: var(--stl-text-muted);
  white-space: nowrap;
}
.stl-sviz-q__row span {
  flex: 0 0 auto;
  width: 16px; height: 16px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--stl-hairline-hi);
  border-radius: 5px;
  background: var(--stl-surface-2);
  font-family: var(--stl-font-mono);
  font-size: 9.5px;
}
.stl-sviz-q__row.is-on { border-color: var(--stl-accent-line); background: var(--stl-accent-soft); color: var(--stl-text); }
.stl-sviz-q__row.is-on span { background: var(--stl-accent); border-color: var(--stl-accent); color: var(--stl-bg-deep); }
.stl-sviz-q__graph {
  flex: 0 0 auto;
  width: 84px; height: 58px;
  padding: 4px;
  border: 1px solid var(--stl-hairline);
  border-radius: 8px;
  background: var(--stl-surface);
}
.stl-step__viz--sig { gap: 8px; }
.stl-sviz-sig {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 11px;
  border: 1px solid;
  border-radius: 9px;
  font-size: 11.5px; font-weight: 600;
  white-space: nowrap;
}
.stl-sviz-sig i { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.stl-sviz-sig small { margin-left: 4px; font-size: 0.84em; font-weight: 500; opacity: 0.58; }
.stl-sviz-sig--g { color: var(--stl-green-text);  border-color: rgba(var(--stl-green-rgb), 0.5);  background: rgba(var(--stl-green-rgb), 0.08); }
.stl-sviz-sig--y { color: var(--stl-yellow-text); border-color: rgba(var(--stl-yellow-rgb), 0.3);  background: rgba(var(--stl-yellow-rgb), 0.04); }
.stl-sviz-sig--r { color: var(--stl-red-text);    border-color: rgba(var(--stl-red-rgb), 0.3); background: rgba(var(--stl-red-rgb), 0.04); }
/* Restored 2026-08-18: these keyframes lived inside the .stl-mock block
 * that v2 deleted (42ea48f), but this consumer survived — the pulse on
 * the marketing results visual had silently become a no-op. Defined
 * next to its only consumer so the next deletion takes both. */
@keyframes stlSigGlow {
  0%, 100% { box-shadow: 0 0 0 rgba(var(--stl-green-rgb), 0); }
  50%      { box-shadow: 0 0 22px -4px var(--stl-green); }
}
.stl-sviz-sig.is-pick { animation: stlSigGlow 2.6s ease-in-out infinite; }
/* Step 3 micro-UI: the completion screen in miniature. */
.stl-sviz-res { display: grid; gap: 9px; min-width: 0; width: 100%; max-width: 240px; }
.stl-sviz-res__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.stl-sviz-res__score {
  font-family: var(--stl-font-display);
  font-size: 26px; font-weight: 700; letter-spacing: -0.02em;
  color: var(--stl-text); line-height: 1;
}
.stl-sviz-res__score i { font-style: normal; font-size: 13px; font-weight: 500; color: var(--stl-text-subtle); }
.stl-sviz-res__btn {
  font-size: 11px; font-weight: 600;
  color: var(--stl-text);
  padding: 5px 10px;
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-pill);
  background: var(--stl-surface);
  white-space: nowrap;
}
.stl-sviz-res__seg { display: flex; gap: 3px; }
.stl-sviz-res__seg i { flex: 1; height: 6px; border-radius: 3px; border: 1.5px solid transparent; }
.stl-sviz-res__seg i[data-s="g1"] { background: var(--stl-green); }
.stl-sviz-res__seg i[data-s="y1"] { background: var(--stl-yellow); }
.stl-sviz-res__seg i[data-s="r1"] { background: var(--stl-red); }
.stl-sviz-res__seg i[data-s="g0"] { border-color: var(--stl-green); }
.stl-sviz-res__seg i[data-s="y0"] { border-color: var(--stl-yellow); }
.stl-sviz-res__seg i[data-s="r0"] { border-color: var(--stl-red); }
.stl-sviz-res__hint {
  font-family: var(--stl-font-mono);
  font-size: 9.5px; letter-spacing: 0.04em;
  color: var(--stl-text-subtle);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stl-step__t {
  margin: 0 0 8px;
  font-family: var(--stl-font-display);
  font-size: 17.5px; font-weight: 600; letter-spacing: -0.01em;
  color: var(--stl-text);
}
.stl-step__p { margin: 0; font-size: 14.5px; line-height: 1.62; color: var(--stl-text-muted); }

/* ---- the six-outcome ledger: results mock + definition cards ---- */
.stl-ledger {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: 22px;
  align-items: start;
  margin-top: 28px;
}
.stl-ledger__mock {
  padding: 24px;
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-lg);
  background:
    linear-gradient(165deg, rgba(148, 163, 184, 0.05) 0%, rgba(255, 255, 255, 0.02) 50%, transparent 80%),
    var(--stl-surface);
  box-shadow: 0 18px 50px -16px rgba(0, 0, 0, 0.4);
}
.stl-ledger__bar {
  display: flex; align-items: center; gap: 9px;
  margin: 0 0 14px;
  font-family: var(--stl-font-mono);
  font-size: 11.5px; letter-spacing: 0.04em;
  color: var(--stl-text-subtle);
}
.stl-ledger__score {
  font-family: var(--stl-font-display);
  font-size: 44px; font-weight: 700; line-height: 1;
  color: var(--stl-text);
  margin-bottom: 14px;
}
.stl-ledger__score span { font-size: 18px; font-weight: 500; color: var(--stl-text-subtle); margin-left: 2px; }
.stl-ledger__seg { display: flex; gap: 4px; margin-bottom: 18px; }
.stl-ledger__seg i { flex: 1; height: 8px; border-radius: 4px; border: 1.5px solid transparent; }
.stl-ledger__seg i[data-s="g1"] { background: var(--stl-green); }
.stl-ledger__seg i[data-s="y1"] { background: var(--stl-yellow); }
.stl-ledger__seg i[data-s="r1"] { background: var(--stl-red); }
.stl-ledger__seg i[data-s="g0"] { border-color: var(--stl-green); }
.stl-ledger__seg i[data-s="y0"] { border-color: var(--stl-yellow); }
.stl-ledger__seg i[data-s="r0"] { border-color: var(--stl-red); }
.stl-ledger__tally { display: grid; gap: 8px; }
.stl-ledger__row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px;
  border: 1px solid var(--stl-hairline);
  border-radius: 10px;
  background: var(--stl-surface-2);
  font-size: 13px; color: var(--stl-text);
}
.stl-ledger__row b { font-weight: 600; white-space: nowrap; }
.stl-ledger__row em { font-style: normal; font-size: 11px; width: 12px; text-align: center; flex: 0 0 auto; }
.stl-ledger__dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.stl-ledger__gauge {
  flex: 1;
  height: 5px;
  border-radius: 3px;
  background: var(--stl-surface-3);
  position: relative;
  overflow: hidden;
  margin: 0 6px;
  min-width: 40px;
}
.stl-ledger__gauge i { position: absolute; top: 0; bottom: 0; left: 0; width: var(--w); border-radius: 3px; }
.stl-ledger__count { font-family: var(--stl-font-mono); font-size: 12px; color: var(--stl-text-muted); min-width: 14px; text-align: right; }
.stl-how__list {
  list-style: none;
  margin: 28px auto 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}
/* Inside the split, the cards run 2-up beside the mock. */
.stl-ledger .stl-how__list { margin: 0; grid-template-columns: 1fr 1fr; }
.stl-how__list li {
  position: relative;
  padding: 18px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background: var(--stl-surface);
  font-size: 14px; line-height: 1.55;
}
.stl-how__list li[data-o="misconception"] {
  border-color: rgba(var(--stl-red-rgb), 0.45);
  background: rgba(var(--stl-red-rgb), 0.05);
}
.stl-how__list b { display: block; margin: 0 0 3px; padding-right: 44px; font-family: var(--stl-font-display); font-weight: 650; font-size: 15px; color: var(--stl-text); }
.stl-how__list span { color: var(--stl-text-muted); min-width: 0; }
.stl-oc { position: absolute; top: 18px; right: 16px; display: inline-flex; align-items: center; gap: 6px; }
.stl-oc i { width: 10px; height: 10px; border-radius: 50%; }
.stl-how__list li[data-o="mastery"] .stl-oc i,
.stl-how__list li[data-o="misconception"] .stl-oc i { background: var(--stl-green); }
.stl-how__list li[data-o="lucky"] .stl-oc i,
.stl-how__list li[data-o="gap"] .stl-oc i { background: var(--stl-yellow); }
.stl-how__list li[data-o="fluke"] .stl-oc i,
.stl-how__list li[data-o="cold"] .stl-oc i { background: var(--stl-red); }
.stl-oc em { font-style: normal; font-size: 12px; line-height: 1; }
.stl-how__list li[data-o="mastery"] .stl-oc em,
.stl-how__list li[data-o="lucky"] .stl-oc em,
.stl-how__list li[data-o="fluke"] .stl-oc em { color: var(--stl-green-text); }
.stl-how__list li[data-o="misconception"] .stl-oc em,
.stl-how__list li[data-o="gap"] .stl-oc em,
.stl-how__list li[data-o="cold"] .stl-oc em { color: var(--stl-red-text); }

/* ---- pillars (band ledes verbatim; see how-page.test.js) ----
 * Three equal cards at desktop instead of stacked rows. */
.stl-how__pillars { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
.stl-how__sec {
  margin: 0;
  padding: 26px 24px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background: var(--stl-surface);
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.35s ease;
}
.stl-how__sec:hover { transform: translateY(-4px); border-color: var(--stl-hairline-hi); }
.stl-how__head { display: flex; align-items: center; gap: 11px; margin-bottom: 12px; }
/* The band-context mark ships margin: 0 0 10px — inside the centered
 * flex head that margin shoves the icon above the title's midline
 * (Josh 2026-08-11). Zero it here. */
.stl-how__head .stl-why__mark { margin: 0; }
.stl-how__t {
  margin: 0;
  font-family: var(--stl-font-display);
  font-size: 19px; font-weight: 600; letter-spacing: -0.01em;
  color: var(--stl-text);
}
.stl-how__lede {
  margin: 0 0 12px;
  font-size: 15px; line-height: 1.55; font-weight: 500;
  color: var(--stl-text);
  max-width: 62ch;
}
.stl-how__p {
  margin: 0 0 12px;
  font-size: 14.5px; line-height: 1.65;
  color: var(--stl-text-muted);
  max-width: 68ch;
}
.stl-how__p:last-child { margin-bottom: 0; }

/* ---- PRICING PLAN CARDS (billing Phase 2, 2026-08-26) --------------
 * Modifiers on .stl-how__sec, NOT a new .stl-plan__* family. Three
 * offerings is exactly what the pillar grid already does, and the
 * standing complaint about this stylesheet is components getting
 * re-implemented under a new name (STYLEGUIDE.md, Rule 4 preamble).
 *
 * The featured card wears the accent as FILL and colour, never extra
 * border weight — rule 2. A thicker border on the recommended plan is
 * the single most common way this component gets built wrong. */
.stl-how__sec--plan { display: flex; flex-direction: column; }
.stl-how__sec--plan .stl-how__head { margin-bottom: 4px; }
.stl-how__sec--feature {
  background:
    linear-gradient(180deg, rgba(var(--stl-green-rgb), 0.07), transparent 60%),
    var(--stl-surface);
  border-color: rgba(var(--stl-green-rgb), 0.32);
}
.stl-how__price { margin: 0 0 10px; display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.stl-how__price-num {
  font-family: var(--stl-font-display);
  font-size: 34px; font-weight: 600; letter-spacing: -0.02em;
  color: var(--stl-text);
}
.stl-how__price-per { font-size: 13px; color: var(--stl-text-muted); }
.stl-how__feats { margin: 4px 0 18px; padding: 0; list-style: none; display: grid; gap: 9px; }
.stl-how__feats li {
  position: relative;
  padding-left: 22px;
  font-size: 14px; line-height: 1.5;
  color: var(--stl-text-muted);
}
/* The tick is the confidence green doing its ordinary "yes, included"
 * job — conventional status, which is one of the three sanctioned
 * meanings, not a fourth. */
.stl-how__feats li::before {
  content: "";
  position: absolute; left: 2px; top: 0.42em;
  width: 9px; height: 5px;
  border-left: 2px solid var(--stl-green);
  border-bottom: 2px solid var(--stl-green);
  transform: rotate(-45deg);
}
/* Pushes the card's footer (button or note) to the bottom so three
 * cards of different heights still line their actions up. */
.stl-how__sec--plan .stl-btn,
.stl-how__plannote { margin-top: auto; }
.stl-how__plannote {
  margin-bottom: 0;
  padding-top: 12px;
  font-size: 13px; font-weight: 500;
  color: var(--stl-text-faint);
}
.stl-how__legal-link { color: var(--stl-text-muted); text-decoration: underline; text-underline-offset: 2px; }
/* "More questions" under the how page's six, and the FAQ screen's own
 * group headings. Same .stl-faq component on both pages — the exhaustive
 * set is a different PAGE, not a different accordion. */
.stl-how__faq-more { margin: 18px 0 0; text-align: center; }
.stl-how__faq-link {
  font-size: 14px;
  color: var(--stl-text-muted);
  text-decoration: underline; text-underline-offset: 3px;
}
.stl-how__faq-link:hover { color: var(--stl-text); }
/* Constrained to the SAME 740px the accordion centres itself at, so the
 * group label lines up with the questions under it. Without this the
 * heading sits out at the section edge and reads as unrelated to the
 * list it introduces. */
.stl-faqp__group { max-width: 740px; margin: 34px auto 0; }
.stl-faqp__group:first-of-type { margin-top: 26px; }
.stl-faqp__group .stl-how__faq { margin-top: 0; }
.stl-faqp__h3 {
  margin: 0 0 12px;
  font-family: var(--stl-font-mono);
  font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--stl-text-faint);
  text-align: left;
}
.stl-how__legal-link:hover { color: var(--stl-text); }

@media (max-width: 980px) {
  .stl-how__price-num { font-size: 30px; }
}

/* ---- CUSTOM TESTS BUILDER (teacher dashboard, db/020) --------------
 * Built on the admin field kit (.stl-field / .stl-admin__input) and the
 * generic buttons; only the three genuinely new things get classes: the
 * mark picker, the tint swatches, and the test cards. Selected state is
 * colour and fill, never border weight (rule 2). */
.stl-ctb { display: grid; gap: 26px; }
.stl-ctb__h { margin: 0 0 14px; font-family: var(--stl-font-display); font-size: 17px; font-weight: 600; color: var(--stl-text); }
.stl-ctb__form {
  padding: 20px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background: var(--stl-surface);
  display: grid; gap: 14px;
  justify-items: start;
}
.stl-ctb__form .stl-admin__row-2 { width: 100%; }
.stl-ctb__form .stl-field { width: 100%; }

.stl-ctb__marks { display: flex; flex-wrap: wrap; gap: 8px; }
.stl-ctb__mark {
  width: 52px; height: 52px;
  display: grid; place-items: center;
  padding: 9px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-md);
  background: var(--stl-surface-2);
  color: var(--stl-text-muted);
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.stl-ctb__mark svg { width: 100%; height: 100%; }
.stl-ctb__mark:hover { color: var(--stl-text); border-color: var(--stl-hairline-hover); }
.stl-ctb__mark.is-on {
  color: var(--stl-accent);
  background: var(--stl-accent-soft);
  border-color: var(--stl-accent-line);
}

.stl-ctb__tints { display: flex; flex-wrap: wrap; gap: 8px; }
.stl-ctb__tint {
  width: 30px; height: 30px;
  border-radius: 50%;
  border: 1px solid var(--stl-hairline);
  background: var(--ct-swatch);
  cursor: pointer;
  transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.stl-ctb__tint:hover { transform: scale(1.08); }
/* Selection = a soft halo in the swatch's own colour — ≥3px and low
 * alpha, which is the sanctioned ring shape (a11y-floor exemption). */
.stl-ctb__tint.is-on { box-shadow: 0 0 0 4px color-mix(in srgb, var(--ct-swatch) 35%, transparent); }

.stl-ctb__list { display: grid; gap: 12px; }
.stl-ctb__card {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 14px 16px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background: var(--stl-surface);
}
.stl-ctb__card.is-selected {
  background: rgba(var(--tile-tint), 0.08);
  border-color: rgba(var(--tile-tint), 0.35);
}
.stl-ctb__card-mark { width: 34px; height: 34px; color: rgb(var(--tile-tint)); flex: 0 0 auto; }
.stl-ctb__card-mark svg { width: 100%; height: 100%; }
.stl-ctb__card-body { flex: 1 1 200px; min-width: 0; }
.stl-ctb__card-name { margin: 0; font-size: 15px; font-weight: 600; color: var(--stl-text); }
.stl-ctb__card-meta { margin: 2px 0 0; font-size: 12.5px; color: var(--stl-text-muted); }
.stl-ctb__card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.stl-ctb__danger { color: var(--stl-red-text); }
.stl-ctb__schema summary { cursor: pointer; font-size: 13px; color: var(--stl-text-muted); }
.stl-hint-inline { font-weight: 400; color: var(--stl-text-faint); }

@media (max-width: 640px) {
  .stl-ctb__card-actions { width: 100%; }
}

/* ---- PLANS (signed-in face of the pricing screen) ------------------
 * Account state above the plan cards. Uses the page's own surfaces and
 * the .stl-pill radius token; the plan badge is a status chip, which is
 * one of the three sanctioned uses of the signal palette (rule 1). */
.stl-plans__acct {
  padding: 24px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background: var(--stl-surface);
  margin-bottom: 32px;
  text-align: left;
}
.stl-plans__acct .stl-how__h2 { text-align: left; font-size: 22px; margin-bottom: 14px; }
.stl-plans__now { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.stl-plans__badge {
  display: inline-flex; align-items: center;
  padding: 5px 12px;
  border-radius: var(--r-pill);
  font-size: 13px; font-weight: 600;
  color: var(--stl-text-muted);
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
}
/* Plus wears the confidence green as conventional "you are good" status,
 * fill and colour only. */
.stl-plans__badge[data-plan="plus"] {
  color: var(--stl-green-text);
  background: rgba(var(--stl-green-rgb), 0.12);
  border-color: rgba(var(--stl-green-rgb), 0.3);
}
.stl-plans__line { margin: 0; font-size: 14.5px; color: var(--stl-text); }
.stl-plans__meter { margin: 12px 0 0; font-size: 13.5px; color: var(--stl-text-muted); }
.stl-plans__acct .stl-btn { margin-top: 16px; }
.stl-plans__h3 {
  margin: 26px 0 10px;
  font-family: var(--stl-font-display);
  font-size: 15px; font-weight: 600; letter-spacing: -0.01em;
  color: var(--stl-text);
}
.stl-plans__invoices { font-size: 13.5px; color: var(--stl-text-muted); }
.stl-plans__inv { list-style: none; margin: 0; padding: 0; }
.stl-plans__inv-row {
  display: grid;
  grid-template-columns: minmax(90px, auto) 1fr auto auto;
  align-items: center; gap: 12px;
  padding: 10px 0;
  border-top: 1px solid var(--stl-hairline);
}
.stl-plans__inv-row:first-child { border-top: 0; }
.stl-plans__inv-date { color: var(--stl-text-muted); }
.stl-plans__inv-desc { color: var(--stl-text); }
.stl-plans__inv-amt { color: var(--stl-text); font-variant-numeric: tabular-nums; }
.stl-plans__inv-link { color: var(--stl-text-muted); text-decoration: underline; text-underline-offset: 2px; }
.stl-plans__inv-link:hover { color: var(--stl-text); }

@media (max-width: 640px) {
  .stl-plans__acct { padding: 18px 16px; }
  /* Amount and receipt drop under the description rather than squeezing
     four columns into a phone. */
  .stl-plans__inv-row { grid-template-columns: 1fr auto; row-gap: 4px; }
  .stl-plans__inv-desc { grid-column: 1 / -1; order: 3; }
}


/* ===================================================================
 * HOW-PAGE ANIMATION PASS (Josh 2026-08-11) — the mock UIs behave
 * like the real thing. Everything is CSS, gated on the .is-rev class
 * the reveal observer already adds, and switched off wholesale under
 * prefers-reduced-motion at the bottom of this block.
 * =================================================================== */

/* 1 ── Hero float pop-in on reveal. The 7s choice/signal loop that used
 * to run here (howHeroChoice / howHeroSig) targeted .stl-mock__*, which
 * v2 deleted (42ea48f) — the hero is the real, playable demo now, so a
 * fake autoplaying interaction on top of it would fight the user.
 * Selectors and their keyframes removed 2026-08-18. */
@keyframes howHeroFloat {
  0%, 52%  { transform: translateY(6px) scale(0.92); opacity: 0.35; }
  60%, 88% { transform: none; opacity: 1; }
  96%, 100% { transform: translateY(6px) scale(0.92); opacity: 0.35; }
}
.stl-how__hero.is-rev .stl-float--g { animation: howHeroFloat 7s ease-in-out infinite; }

/* 2 ── Twins: ticks pop in one at a time once the block reveals. */
.stl-how__block .stl-twin__ticks i { opacity: 0; transform: scale(0.4); }
.stl-how__block.is-rev .stl-twin__ticks i { animation: howTickPop .32s var(--ease-spring) forwards; }
@keyframes howTickPop { to { opacity: 1; transform: none; } }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(1)  { animation-delay: .15s; }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(2)  { animation-delay: .21s; }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(3)  { animation-delay: .27s; }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(4)  { animation-delay: .33s; }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(5)  { animation-delay: .39s; }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(6)  { animation-delay: .45s; }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(7)  { animation-delay: .51s; }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(8)  { animation-delay: .57s; }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(9)  { animation-delay: .63s; }
.stl-how__block.is-rev .stl-twin__ticks i:nth-child(10) { animation-delay: .69s; }

/* 3 ── Step 1: the graph draws itself on reveal. */
.stl-sviz-q__graph path { stroke-dasharray: 90; stroke-dashoffset: 90; }
.stl-sviz-q__graph circle { opacity: 0; transform: scale(0); transform-box: fill-box; transform-origin: center; }
.stl-how__block.is-rev .stl-sviz-q__graph path:nth-of-type(1) { animation: howDraw .8s ease .2s forwards; }
.stl-how__block.is-rev .stl-sviz-q__graph path:nth-of-type(2) { animation: howDraw .8s ease .5s forwards; }
.stl-how__block.is-rev .stl-sviz-q__graph circle { animation: howTickPop .3s var(--ease-spring) 1.15s forwards; }
@keyframes howDraw { to { stroke-dashoffset: 0; } }

/* 4 ── Step 2: the green pill keeps getting picked. */
@keyframes howSigPick {
  0%, 76%, 100% { transform: none; }
  82% { transform: scale(0.94); }
  88% { transform: scale(1.02); }
}
.stl-how__block.is-rev .stl-sviz-sig--g.is-pick { animation: howSigPick 4.2s ease-in-out infinite; }

/* 5 ── Step 3: the completion strip fills, the review pill breathes. */
.stl-sviz-res__seg i { opacity: 0; transform: scaleX(0.3); transform-origin: left center; }
.stl-how__block.is-rev .stl-sviz-res__seg i { animation: howTickPop .3s var(--ease-spring) forwards; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(1)  { animation-delay: .20s; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(2)  { animation-delay: .26s; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(3)  { animation-delay: .32s; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(4)  { animation-delay: .38s; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(5)  { animation-delay: .44s; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(6)  { animation-delay: .50s; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(7)  { animation-delay: .56s; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(8)  { animation-delay: .62s; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(9)  { animation-delay: .68s; }
.stl-how__block.is-rev .stl-sviz-res__seg i:nth-child(10) { animation-delay: .74s; }
@keyframes howBtnBreathe {
  0%, 100% { box-shadow: none; }
  50% { box-shadow: 0 0 0 3px rgba(var(--stl-green-rgb), 0.14); }
}
.stl-how__block.is-rev .stl-sviz-res__btn { animation: howBtnBreathe 3.6s ease-in-out infinite; }

/* 6 ── Ledger: gauges grow to their widths; strip staggers in. */
.stl-ledger__gauge i { width: 0; transition: width .9s var(--ease-out); }
.stl-how__block.is-rev .stl-ledger__gauge i { width: var(--w, 0%); }
.stl-how__block.is-rev .stl-ledger__row:nth-child(1) .stl-ledger__gauge i { transition-delay: .25s; }
.stl-how__block.is-rev .stl-ledger__row:nth-child(2) .stl-ledger__gauge i { transition-delay: .35s; }
.stl-how__block.is-rev .stl-ledger__row:nth-child(3) .stl-ledger__gauge i { transition-delay: .45s; }
.stl-how__block.is-rev .stl-ledger__row:nth-child(4) .stl-ledger__gauge i { transition-delay: .55s; }
.stl-how__block.is-rev .stl-ledger__row:nth-child(5) .stl-ledger__gauge i { transition-delay: .65s; }
.stl-how__block.is-rev .stl-ledger__row:nth-child(6) .stl-ledger__gauge i { transition-delay: .75s; }
.stl-ledger__seg i { opacity: 0; transform: scaleX(0.3); transform-origin: left center; }
.stl-how__block.is-rev .stl-ledger__seg i { animation: howTickPop .3s var(--ease-spring) forwards; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(1)  { animation-delay: .10s; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(2)  { animation-delay: .16s; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(3)  { animation-delay: .22s; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(4)  { animation-delay: .28s; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(5)  { animation-delay: .34s; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(6)  { animation-delay: .40s; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(7)  { animation-delay: .46s; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(8)  { animation-delay: .52s; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(9)  { animation-delay: .58s; }
.stl-how__block.is-rev .stl-ledger__seg i:nth-child(10) { animation-delay: .64s; }

/* 7 ── AI radar: the polygon draws, the fill fades up, dots pop. */
.stl-hana__poly { stroke-dasharray: 620; stroke-dashoffset: 620; fill-opacity: 0; }
.stl-how__block.is-rev .stl-hana__poly {
  animation: howRadarDraw 1.1s ease .25s forwards, howRadarFill .6s ease 1.15s forwards;
}
@keyframes howRadarDraw { to { stroke-dashoffset: 0; } }
@keyframes howRadarFill { to { fill-opacity: 0.14; } }
/* Pop rides the <g> wrapper; the LIVE MORPH below owns the circle's
 * own transform — separate elements so the two never fight. */
.stl-hana__dotw { opacity: 0; transform: scale(0); transform-box: fill-box; transform-origin: center; }
.stl-how__block.is-rev .stl-hana__dotw { animation: howTickPop .3s var(--ease-spring) forwards; }
.stl-how__block.is-rev .stl-hana__dotw:nth-of-type(1) { animation-delay: 1.20s; }
.stl-how__block.is-rev .stl-hana__dotw:nth-of-type(2) { animation-delay: 1.28s; }
.stl-how__block.is-rev .stl-hana__dotw:nth-of-type(3) { animation-delay: 1.36s; }
.stl-how__block.is-rev .stl-hana__dotw:nth-of-type(4) { animation-delay: 1.44s; }
.stl-how__block.is-rev .stl-hana__dotw:nth-of-type(5) { animation-delay: 1.52s; }
.stl-how__block.is-rev .stl-hana__dotw:nth-of-type(6) { animation-delay: 1.60s; }

/* LIVE MORPH (Josh 2026-08-11): the radar cycles three mastery
 * snapshots — JS bumps data-hstate on the svg every few seconds and
 * swaps the chip numbers to match; the shape morphs via the CSS `d`
 * property, the dots ride translate deltas. Browsers without CSS-d
 * support simply keep the state-0 shape. */
/* (Both the polygon AND the dots tween in JS off the same vertex
 * array — cx/cy attributes set per frame — so the line and its dots
 * can never drift apart. Safari has no CSS `d` property, and the two
 * CSS transition curves never matched the JS ease exactly.) */
.stl-hana__chip { opacity: 0; transform: translateY(6px); }
.stl-how__block.is-rev .stl-hana__chip { animation: howTickPop .4s var(--ease-spring) forwards; }
.stl-how__block.is-rev .stl-hana__sw-row:nth-child(1) .stl-hana__chip:nth-of-type(1) { animation-delay: 1.5s; }
.stl-how__block.is-rev .stl-hana__sw-row:nth-child(1) .stl-hana__chip:nth-of-type(2) { animation-delay: 1.58s; }
.stl-how__block.is-rev .stl-hana__sw-row:nth-child(2) .stl-hana__chip:nth-of-type(1) { animation-delay: 1.66s; }
.stl-how__block.is-rev .stl-hana__sw-row:nth-child(2) .stl-hana__chip:nth-of-type(2) { animation-delay: 1.74s; }

/* 8 ── Teacher carousel: each slide animates as it becomes active. */
.stl-teachx__bar em b { width: 0; transition: width .8s var(--ease-out) .25s; }
.stl-teachx__slide.is-active .stl-teachx__bar em b { width: var(--w, 40%); }
.stl-teachx__slide .stl-teachx__wkcell.is-on,
.stl-teachx__slide .stl-teachx__ticks u,
.stl-teachx__slide .stl-teachx__linkrow { opacity: 0; transform: scale(0.85); }
.stl-teachx__slide.is-active .stl-teachx__wkcell.is-on,
.stl-teachx__slide.is-active .stl-teachx__ticks u,
.stl-teachx__slide.is-active .stl-teachx__linkrow { animation: howTickPop .35s var(--ease-spring) forwards; }
.stl-teachx__slide.is-active .stl-teachx__linkrow { animation-delay: .8s; }
.stl-teachx__slide.is-active .stl-teachx__wkcell.is-on { animation-delay: .45s; }
.stl-teachx__slide.is-active .stl-teachx__wkcard:nth-of-type(2) .stl-teachx__wkcell.is-on { animation-delay: .65s; }
.stl-teachx__slide.is-active .stl-teachx__ticks u:nth-child(1) { animation-delay: .35s; }
.stl-teachx__slide.is-active .stl-teachx__ticks u:nth-child(2) { animation-delay: .43s; }
.stl-teachx__slide.is-active .stl-teachx__ticks u:nth-child(3) { animation-delay: .51s; }
.stl-teachx__slide.is-active .stl-teachx__ticks u:nth-child(4) { animation-delay: .59s; }

/* ── Reduced motion: everything above turns off, end states apply. ── */
@media (prefers-reduced-motion: reduce) {
  .stl-how__hero.is-rev .stl-mock__choice.is-on,
  .stl-how__hero.is-rev .stl-mock__sig--g,
  .stl-how__hero.is-rev .stl-float--g,
  .stl-how__block.is-rev .stl-sviz-sig--g.is-pick,
  .stl-how__block.is-rev .stl-sviz-res__btn { animation: none; }
  .stl-how__block .stl-twin__ticks i,
  .stl-sviz-q__graph circle,
  .stl-sviz-res__seg i,
  .stl-ledger__seg i,
  .stl-hana__dotw,
  .stl-hana__chip,
  .stl-teachx__slide .stl-teachx__wkcell.is-on,
  .stl-teachx__slide .stl-teachx__ticks u,
  .stl-teachx__slide .stl-teachx__linkrow { opacity: 1; transform: none; animation: none; }
  .stl-sviz-q__graph path { stroke-dasharray: none; stroke-dashoffset: 0; animation: none; }
  .stl-hana__poly { stroke-dasharray: none; stroke-dashoffset: 0; fill-opacity: 0.14; animation: none; transition: none; }

  .stl-ledger__gauge i, .stl-teachx__bar em b { transition: none; }
  .stl-teachx__track { transition: none; }
}

/* ---- AI analysis section (how page) ---- */
/* Split mirrors .stl-ledger but flipped: the mock (radar card + CTA)
 * leads, the three numbered points ride shotgun. The radar card
 * extrapolates .stl-radar-section__joined: neutral slate sheen (glass
 * never takes a tint), hairline divider, SW rows beneath. */
.stl-how__analysis {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  grid-template-areas:
    "card points"
    "cta  .";
  column-gap: 26px;
  align-items: center;
  margin-top: 28px;
}
.stl-hana__card {
  grid-area: card;
  width: 100%;
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-lg);
  background:
    radial-gradient(ellipse at top left, rgba(148, 163, 184, 0.06), transparent 60%),
    var(--stl-surface);
  box-shadow: 0 18px 50px -16px rgba(0, 0, 0, 0.4);
  overflow: hidden;
}
.stl-hana__chart { padding: 16px 16px 6px; }
.stl-hana__eyebrow {
  margin: 0;
  display: flex; align-items: center; gap: 8px;
  font-family: var(--stl-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
}
/* Rainbow pill dot — the All-view eyebrow marker from the real
 * Analysis screen (a specific test would use a round tinted dot). */
.stl-hana__eyebrow i {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--stl-rainbow);
}
.stl-hana__radar {
  display: block;
  width: 100%;
  max-width: 360px;
  height: auto;
  margin: 4px auto 0;
  color: var(--stl-text);
}
.stl-hana__radar text {
  font: 500 9px var(--stl-font-mono);
  letter-spacing: 0.04em;
  fill: var(--stl-text-subtle);
}
.stl-hana__divider { height: 1px; background: var(--stl-hairline); }
.stl-hana__sw { padding: 14px 16px 16px; display: grid; gap: 10px; }
.stl-hana__sw-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.stl-hana__sw-label {
  font-family: var(--stl-font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--stl-text-subtle);
  min-width: 7.5em;
}
.stl-hana__chip {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 4px 10px;
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-pill);
  background: var(--stl-surface-2);
  font-size: 12.5px;
}
.stl-hana__chip b { font-weight: 500; }
.stl-hana__chip em {
  font-style: normal;
  font-family: var(--stl-font-mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.stl-hana__chip--g em { color: var(--stl-green-text); }
.stl-hana__chip--r em { color: var(--stl-red-text); }
.stl-hana__cta { grid-area: cta; justify-self: center; align-self: start; margin-top: 16px; }
.stl-hana__points { grid-area: points; }
.stl-hana__points {
  list-style: none;
  margin: 0; padding: 0;
  display: grid; gap: 20px;
}
.stl-hana__points li {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 14px;
  align-items: start;
}
/* Bare icons (Josh 2026-08-11, second pass: no chip border, no color
 * — the standard muted gray, one size up from the 15px chip glyphs). */
.stl-hana__ico {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px;
  color: var(--stl-text-muted);
}
.stl-hana__ico svg { width: 20px; height: 20px; }
.stl-hana__t { margin: 0 0 4px; font-size: 15.5px; font-weight: 600; letter-spacing: -0.01em; }
.stl-hana__p { margin: 0; color: var(--stl-text-muted); font-size: 14px; line-height: 1.6; }
@media (max-width: 860px) {
  .stl-how__analysis {
    grid-template-columns: 1fr;
    grid-template-areas: "card" "cta" "points";
    row-gap: 22px;
  }
  .stl-hana__cta { margin-top: 0; }
  .stl-hana__sw-label { min-width: 100%; }
}

/* ---- Built for classrooms (Josh 2026-08-11) — three alternating
 * mock+text rows extrapolating the composer, the worksheet builder,
 * and the roster. Mocks are decorative (aria-hidden). ---- */
.stl-teachx { display: grid; gap: 26px; margin-top: 28px; }
/* Carousel form (Josh 2026-08-11): one tool at a time, so the page
 * stays short. Flex track = every slide stretches to the tallest, so
 * the viewport never changes height mid-slide. */
.stl-teachx--carousel { gap: 18px; margin-top: 22px; }
.stl-teachx__viewport { overflow: hidden; }
.stl-teachx__track {
  display: flex;
  transition: transform .55s var(--ease-spring);
}
.stl-teachx__slide { flex: 0 0 100%; min-width: 0; }
.stl-teachx__nav { display: flex; align-items: center; justify-content: center; gap: 14px; }
.stl-teachx__arrow {
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--stl-hairline);
  border-radius: 50%;
  background: var(--stl-surface);
  color: var(--stl-text-muted);
  cursor: pointer;
}
.stl-teachx__arrow:hover { border-color: var(--stl-hairline-hi); color: var(--stl-text); }
.stl-teachx__dots { display: flex; gap: 8px; }
.stl-teachx__dot {
  width: 8px; height: 8px; padding: 0;
  border-radius: 50%;
  border: 0; cursor: pointer;
  background: var(--stl-hairline-hi);
  transition: background var(--dur-base) var(--ease-spring), transform var(--dur-base) var(--ease-spring);
}
.stl-teachx__dot.is-on { background: #cbd5e1; transform: scale(1.25); }
.stl-teachx__row {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: 26px; align-items: center;
}
.stl-teachx__row--flip .stl-teachx__mock { order: 2; }
.stl-teachx__row--flip .stl-teachx__txt { order: 1; }
.stl-teachx__mock {
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-lg);
  background:
    linear-gradient(165deg, rgba(148, 163, 184, 0.05) 0%, rgba(255, 255, 255, 0.02) 50%, transparent 80%),
    var(--stl-surface);
  box-shadow: 0 18px 50px -16px rgba(0, 0, 0, 0.4);
  padding: 18px;
  display: grid; gap: 10px;
}
.stl-teachx__mocktag {
  margin: 0 0 2px;
  font-family: var(--stl-font-mono);
  font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-teachx__field {
  display: grid; gap: 4px;
  padding: 9px 12px;
  border: 1px solid var(--stl-hairline);
  border-radius: 10px;
  background: var(--stl-bg);
}
.stl-teachx__field span { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--stl-text-subtle); font-family: var(--stl-font-mono); }
.stl-teachx__field i { font-style: normal; font-size: 13.5px; color: var(--stl-text); }
.stl-teachx__bars { display: grid; gap: 7px; }
.stl-teachx__bar { display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--stl-text-muted); }
.stl-teachx__bar span { width: 4.5em; }
.stl-teachx__bar em { flex: 1; height: 6px; border-radius: 3px; background: var(--stl-surface-2); overflow: hidden; display: block; }
.stl-teachx__bar em b { display: block; height: 100%; width: var(--w, 40%); border-radius: 3px; }
.stl-teachx__bar u { text-decoration: none; font-variant-numeric: tabular-nums; color: var(--stl-text); width: 1.5em; text-align: right; }
/* Neutral highlight (Josh 2026-08-11): marketing surfaces use light
 * gray for active/highlight states — color only means something in a
 * real test (test tint) or a signal verdict. */
.stl-teachx__linkrow {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px;
  border: 1px solid rgba(148, 163, 184, 0.45);
  border-radius: 10px;
  background: rgba(148, 163, 184, 0.08);
}
.stl-teachx__linkrow i { font-style: normal; flex: 1; font-family: var(--stl-font-mono); font-size: 12px; color: var(--stl-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stl-teachx__linkrow span { font-size: 11.5px; font-weight: 600; color: #cbd5e1; }
.stl-teachx__wkcard {
  border: 1px solid var(--stl-hairline);
  border-radius: 10px;
  background: var(--stl-bg);
  padding: 10px 12px;
  display: grid; gap: 8px;
}
.stl-teachx__wkcard p { margin: 0; font-size: 13px; color: var(--stl-text); display: flex; align-items: center; gap: 8px; }
.stl-teachx__wkcells { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.stl-teachx__wkcell {
  height: 30px; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  font-size: 11.5px; font-weight: 500;
  border: 1px solid var(--stl-hairline);
}
.stl-teachx__wkcell[data-t="e"] { color: var(--stl-green-text); border-color: rgba(var(--stl-green-rgb), 0.35); }
.stl-teachx__wkcell[data-t="m"] { color: var(--stl-yellow-text); border-color: rgba(var(--stl-yellow-rgb), 0.32); }
.stl-teachx__wkcell[data-t="h"] { color: var(--stl-red-text); border-color: rgba(var(--stl-red-rgb), 0.35); }
.stl-teachx__wkcell[data-t="e"].is-on { background: rgba(var(--stl-green-rgb), 0.14); border-color: var(--stl-green); }
.stl-teachx__wkcell[data-t="m"].is-on { background: rgba(var(--stl-yellow-rgb), 0.12); border-color: var(--stl-yellow); }
.stl-teachx__wkcell[data-t="h"].is-on { background: rgba(var(--stl-red-rgb), 0.14); border-color: var(--stl-red); }
.stl-teachx__gen { justify-self: center; pointer-events: none; margin-top: 2px; }
.stl-teachx__stu {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px;
  border: 1px solid var(--stl-hairline);
  border-radius: 10px;
  background: var(--stl-bg);
}
.stl-teachx__stu > i {
  font-style: normal; flex: none;
  width: 30px; height: 30px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 10.5px; font-weight: 600;
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline);
  color: var(--stl-text-muted);
}
.stl-teachx__stu b { font-size: 13px; font-weight: 500; flex: 1; }
.stl-teachx__ticks { display: flex; gap: 4px; }
.stl-teachx__ticks u { width: 12px; height: 12px; border-radius: 4px; border: 2px solid transparent; }
.stl-teachx__ticks u[data-t="g1"] { background: var(--stl-green); }
.stl-teachx__ticks u[data-t="y1"] { background: var(--stl-yellow); }
.stl-teachx__ticks u[data-t="r1"] { background: var(--stl-red); }
.stl-teachx__ticks u[data-t="g0"] { border-color: var(--stl-green); }
.stl-teachx__ticks u[data-t="y0"] { border-color: var(--stl-yellow); }
.stl-teachx__ticks u[data-t="r0"] { border-color: var(--stl-red); }
.stl-teachx__stu em { font-style: normal; font-weight: 700; font-size: 15px; }
.stl-teachx__stu em i { font-style: normal; font-weight: 500; font-size: 11px; color: var(--stl-text-subtle); }
.stl-teachx__t { margin: 0 0 6px; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.stl-teachx__p { margin: 0; color: var(--stl-text-muted); font-size: 14.5px; line-height: 1.6; max-width: 46ch; }
.stl-teachx__cta { display: flex; justify-content: center; margin: 4px 0 0; }
.stl-teachx__cta .stl-btn { font-size: 15px; padding: 12px 26px; border-radius: var(--r-pill); }
@media (max-width: 860px) {
  .stl-teachx__row { grid-template-columns: 1fr; gap: 16px; }
  .stl-teachx__row--flip .stl-teachx__mock { order: 1; }
  .stl-teachx__row--flip .stl-teachx__txt { order: 2; }
}

/* ---- teacher band ---- */
.stl-how__teach {
  display: flex; align-items: center; justify-content: space-between;
  gap: 28px; flex-wrap: wrap;
  padding: clamp(24px, 3.5vw, 40px);
  border: 1px solid var(--stl-accent-line);
  border-radius: var(--r-lg);
  background: linear-gradient(135deg, rgba(var(--stl-rainbow-mid-rgb), 0.14) 0%, rgba(93, 141, 255, 0.06) 60%, transparent 100%);
}
.stl-how__teach-txt { flex: 1 1 420px; }
/* Pill + a size up (Josh 2026-08-10) — it's the band's only action. */
.stl-how__teach > .stl-btn { font-size: 16px; padding: 13px 28px; border-radius: var(--r-pill); }
.stl-how__teach .stl-how__kicker { text-align: left; }
.stl-how__teach .stl-how__h2 { margin: 0 0 8px; max-width: none; text-align: left; }
.stl-how__teach .stl-how__p { margin: 0; }

/* ---- FAQ ---- */
.stl-how__faq { max-width: 740px; margin: 20px auto 0; border-top: 1px solid var(--stl-hairline); }
.stl-faq { border-bottom: 1px solid var(--stl-hairline); }
.stl-faq summary {
  list-style: none;
  cursor: pointer;
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 16px 2px;
  font-size: 15px; font-weight: 550;
  color: var(--stl-text);
}
.stl-faq summary::-webkit-details-marker { display: none; }
.stl-faq__x { position: relative; flex: 0 0 auto; width: 20px; height: 20px; transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
.stl-faq__x::before, .stl-faq__x::after {
  content: "";
  position: absolute; top: 50%; left: 50%;
  width: 12px; height: 2px;
  background: var(--stl-text-subtle);
  border-radius: 2px;
  transform: translate(-50%, -50%);
}
.stl-faq__x::after { transform: translate(-50%, -50%) rotate(90deg); }
.stl-faq[open] .stl-faq__x { transform: rotate(45deg); }
.stl-faq__a { margin: 0; padding: 0 28px 16px 2px; font-size: 14.5px; line-height: 1.65; color: var(--stl-text-muted); max-width: 68ch; }

/* ---- closing CTA ---- */
.stl-how__cta {
  margin-top: clamp(64px, 9vw, 110px);
  padding: clamp(44px, 6vw, 64px) 24px 8px;
  border-top: 1px solid var(--stl-hairline);
  text-align: center;
}
.stl-how__dots { display: inline-flex; gap: 10px; margin-bottom: 18px; }
.stl-how__dots i { width: 12px; height: 12px; border-radius: 50%; }
.stl-how__dots i:nth-child(1) { background: var(--stl-green);  animation: stlHowPulse 2.7s ease-in-out infinite; }
.stl-how__dots i:nth-child(2) { background: var(--stl-yellow); animation: stlHowPulse 2.7s ease-in-out 0.3s infinite; }
.stl-how__dots i:nth-child(3) { background: var(--stl-red);    animation: stlHowPulse 2.7s ease-in-out 0.6s infinite; }
@keyframes stlHowPulse {
  0%, 100% { transform: scale(1); box-shadow: 0 0 0 rgba(0, 0, 0, 0); }
  25%      { transform: scale(1.3); box-shadow: 0 0 16px currentColor; }
  50%      { transform: scale(1); }
}
.stl-how__dots--sm i { width: 10px; height: 10px; }
.stl-how__cta-t {
  margin: 0 0 10px;
  font-family: var(--stl-font-display);
  font-size: clamp(28px, 4vw, 42px); font-weight: 700; letter-spacing: -0.022em;
  color: var(--stl-text);
}
.stl-how__cta-p { margin: 0 0 24px; font-size: 16px; color: var(--stl-text-muted); }
.stl-how__legal { margin: 32px auto 0; max-width: 62ch; font-size: 11.5px; line-height: 1.55; color: var(--stl-text-faint); }

/* ---- "The signal, in your pocket" — the iOS app section ----------------
 * (Josh 2026-08-20; real screenshots 2026-08-25.)
 *
 * `.stl-pm` is the iPhone FRAME — bezel, radius, shadow — and the screen
 * inside it is a real capture of the shipped app (img/app/*.jpg, taken
 * in the simulator; the HTML re-creations that used to live here are
 * archived in redesigns/homepage-app/phone-mock.css). Every dimension is
 * in em tied to `--stl-pm-w`, so one variable sizes the whole phone.
 * If you need a phone mock anywhere, use this frame — do not draw a
 * second one (GAPS.md §2).
 *
 * `.stl-appx-*` is the section that hosts three of them. Marketing-only;
 * nothing in the app chrome may depend on it. */
.stl-pm {
  --stl-pm-w: 268px;
  width: var(--stl-pm-w); flex: 0 0 auto;
  border-radius: calc(var(--stl-pm-w) * 0.155);
  padding: calc(var(--stl-pm-w) * 0.034);
  background: linear-gradient(160deg, #2b2e38 0%, #14161c 42%, #0c0d12 100%);
  box-shadow:
    0 0 0 1px rgba(255,255,255,.07),
    0 40px 90px -30px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.10);
  position: relative;
}
/* THE ASPECT LIVES ON THE SCREEN, NOT THE FRAME (Josh 2026-08-25: "they
 * seem to be missing some of the padding in the real app"). The frame
 * used to carry aspect-ratio 9/19.5, which made the screen box that
 * ratio MINUS the bezel padding — a hair narrower than the capture's
 * true 1206x2622 — and object-fit: cover paid the difference by
 * cropping ~1.7% off EACH side. That is ~7pt a side on device: half
 * the app's 16pt content gutter, silently eaten. The screen now
 * declares the capture's exact ratio (1206/2622 = 201/437) and the
 * frame wraps it, so cover has nothing left to crop. If a re-shoot
 * changes device (different capture ratio), update this ratio with it. */
.stl-pm__screen {
  width: 100%;
  aspect-ratio: 201 / 437;
  border-radius: calc(var(--stl-pm-w) * 0.125);
  overflow: hidden; position: relative;
  background: #0a0b12;   /* paints while the capture loads */
}
.stl-pm__shot { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The section around the phones. */
.stl-appx__fan {
  display: flex; justify-content: center; align-items: flex-end;
  gap: 22px; margin: 26px 0 8px;
}
.stl-appx__fan .stl-pm { --stl-pm-w: 218px; }
.stl-appx__fan .stl-pm:nth-child(1) { transform: rotate(-4deg) translateY(14px); }
.stl-appx__fan .stl-pm:nth-child(3) { transform: rotate(4deg) translateY(14px); }
/* The three feature blurbs are gone (Josh 2026-08-25) — the sub-line
 * carries the pitch and the screenshots carry the proof. The badge sits
 * between the sub and the phones now: it is the section's one action,
 * and putting it before the art means it is on screen the moment the
 * headline is, not four hundred pixels of phone later. */
.stl-appx__cta { display: flex; justify-content: center; margin: 22px 0 6px; }

/* The store badge — Apple's OFFICIAL artwork, self-hosted at
 * img/app-store-badge.svg (their guidelines forbid REDRAWING the badge,
 * which is exactly what the first draft of this block did; it drew one.
 * They publish the SVG for this use, so we use theirs verbatim).
 * No hover recolour: the badge's look is Apple's, not ours to restyle —
 * feedback is opacity only. The href stays dead until the app clears
 * review; swap in the listing URL then. */
.stl-appstore { display: inline-flex; transition: opacity var(--dur-base) var(--ease-out); }
.stl-appstore:hover { opacity: .85; }
.stl-appstore:focus-visible { outline: 2px solid var(--stl-accent); outline-offset: 3px; border-radius: 8px; }
.stl-appstore img { display: block; height: 44px; width: auto; }
/* In the hero CTA row the badge height-matches the rainbow pill (option
 * 1 of the hero-badge study, Josh 2026-08-25): 14px padding ×2 + the
 * 16.5px label ≈ 50px, minus 2px so Apple's own border does not read
 * taller than the pill. */
.stl-how__hero-cta .stl-appstore--hero img { height: 48px; }

@media (max-width: 980px) {
  .stl-appx__fan { flex-wrap: wrap; gap: 14px; }
  .stl-appx__fan .stl-pm { --stl-pm-w: 190px; }
  .stl-appx__fan .stl-pm:nth-child(1),
  .stl-appx__fan .stl-pm:nth-child(3) { transform: none; }
}
@media (max-width: 520px) {
  /* Two phones fit a phone; three stack into a scroll of screenshots.
   * The PRACTICE mock is the one that folds away — quiz and results
   * are the signal story, picking a test is not. Sizes are checked
   * against the SHELL, not the viewport: at a 360px viewport the card's
   * content box is ~278px, so 158px phones (2×158+14 = 330) overflowed
   * it. 130px pairs fit down to a 320px viewport. */
  .stl-appx__fan .stl-pm:nth-child(1) { display: none; }
  .stl-appx__fan { gap: 12px; }
  .stl-appx__fan .stl-pm { --stl-pm-w: min(150px, 34vw); }
}

/* ---- Michael Weber — the authority section (Josh 2026-08-26) ------
 * One quiet card: his photo, his words verbatim, his name. The circle
 * crop is ours (border-radius on a rect source), ringed with the same
 * hairline every card wears; the quote runs in the page's body voice,
 * larger than a caption because it IS the section's content. */
.stl-mweb {
  display: flex; align-items: center; gap: clamp(22px, 4vw, 40px);
  max-width: 74ch; margin: 30px auto 0;
  padding: clamp(20px, 3vw, 30px);
  border: 1px solid var(--stl-hairline);
  border-radius: var(--r-lg);
  background:
    linear-gradient(165deg, rgba(148, 163, 184, 0.05) 0%, rgba(255, 255, 255, 0.02) 50%, transparent 80%),
    var(--stl-surface);
}
.stl-mweb__photo {
  flex: 0 0 auto;
  /* ONE variable drives both axes, so the frame cannot drift out of
   * square and stop being a circle. That is the whole failure mode
   * here: two independently-set dimensions, or an aspect-ratio silently
   * ignored because the <img> carries height="480" as a presentational
   * attribute (aspect-ratio only applies when one axis is auto). */
  --mweb-photo: clamp(132px, 12vw, 150px);
  width: var(--mweb-photo);
  height: var(--mweb-photo);
  border-radius: 50%;
  object-fit: cover;
  /* The source is a tight 350x480 headshot; centre-top keeps the crop on
   * his face rather than his collar. */
  object-position: 50% 22%;
  border: 1px solid var(--stl-hairline-hi);
  box-shadow: 0 14px 40px -14px rgba(0, 0, 0, 0.55);
}
.stl-mweb__quote { margin: 0; display: grid; gap: 10px; }
.stl-mweb__quote p {
  margin: 0;
  font-size: 14.5px; line-height: 1.65;
  color: var(--stl-text-muted);
}
.stl-mweb__attr {
  margin-top: 4px;
  font-size: 14px; font-weight: 620; color: var(--stl-text);
}
.stl-mweb__attr span {
  display: block; margin-top: 2px;
  font-family: var(--stl-font-mono);
  font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--stl-text-subtle);
}
@media (max-width: 640px) {
  .stl-mweb { flex-direction: column; text-align: center; }
  /* Retune the ONE variable; never set width or height directly here,
     or the circle stops being round. */
  .stl-mweb__photo { --mweb-photo: 120px; }
}

/* ---- tablet ---- */
@media (max-width: 980px) {
  .stl-how__hero { grid-template-columns: 1fr; text-align: center; padding-top: 12px; }
  .stl-how__hero-cta, .stl-how__scope { justify-content: center; }
  .stl-how__h1, .stl-how__hero-sub { margin-left: auto; margin-right: auto; }
  .stl-how__art { min-height: 380px; margin-top: 8px; }
  .stl-how__pillars { grid-template-columns: minmax(0, 1fr); }
  /* minmax(0,…), not bare 1fr — bare 1fr means minmax(auto,1fr), and
   * auto re-admits min-content sizing: the desktop rule already knew
   * this, the mobile overrides forgot, and that is half of how the
   * page ended up 471px wide on a 360px Android. */
  .stl-how__steps { grid-template-columns: minmax(0, 1fr); max-width: 640px; }
  .stl-how__list { grid-template-columns: 1fr 1fr; }
  .stl-ledger { grid-template-columns: minmax(0, 1fr); }
}

/* ---- phones ---- */
@media (max-width: 640px) {
  .stl-how__block { margin-top: 44px; }
  .stl-how__h2 { font-size: 22px; }
  .stl-how__art { min-height: 330px; }
  /* ALL the floating verdict chips fold away on phones (Josh's Android
   * pass, 2026-08-25 — was --r only). At phone width the demo card
   * fills the art box, so a chip "floating beside" the card has nowhere
   * to float — the yellow one sat directly ON the confidence buttons.
   * The demo produces these verdicts itself when answered; losing the
   * preview costs nothing. */
  .stl-float { display: none; }
  .stl-how__twins { grid-template-columns: minmax(0, 1fr); }
  .stl-how__steps { grid-template-columns: minmax(0, 1fr); }
  .stl-how__list { grid-template-columns: 1fr; }
  .stl-how__t { font-size: 17px; }
  .stl-how__lede { font-size: 15px; }
  /* Kept as a literal rule — how-page.test.js greps for it. */
  .stl-how__list li { display: block; }
  .stl-how__list b { min-width: 0; margin-right: 6px; }
  .stl-ledger .stl-how__list { grid-template-columns: 1fr; }
  .stl-ledger__mock { padding: 18px; }
  .stl-demo { padding: 18px; }
  .stl-demo__choices { grid-template-columns: 1fr; }
  .stl-how__teach { padding: 22px; }

  /* Step 2's three signal pills were THE page-widener: three nowrap
   * pills + colour words + viz padding = 405px of unbreakable content.
   * The colour words fold away (same trade the hero demo made) and the
   * row may wrap 2+1 on the narrowest phones rather than clip. */
  .stl-sviz-sig small { display: none; }
  /* Sized so the three pills hold ONE line down to a 320px viewport —
   * at 7px/11px they measured 256 against ~260 available and wrapped
   * on rounding, which left "Don't know" stranded on its own line.
   * flex-wrap stays as the failsafe for narrower-than-320 windows. */
  .stl-sviz-sig { padding: 6px 8px; font-size: 10.5px; gap: 5px; }
  .stl-step__viz--sig { gap: 7px; flex-wrap: wrap; padding: 14px 10px; }

  /* Teachers carousel had NO phone rules at all: the two-column rows
   * never collapsed, so the worksheet-matrix mock burst out of a
   * ~150px column by twice its width. One column, text first (the
   * mocks are decoration; the sentence is the argument). */
  .stl-teachx__row { grid-template-columns: minmax(0, 1fr); gap: 14px; }
  .stl-teachx__row .stl-teachx__txt { order: 1; }
  .stl-teachx__row .stl-teachx__mock { order: 2; }
  .stl-teachx__mock { padding: 14px; }
}

/* Motion collapses entirely for reduced-motion users: everything is
 * simply visible, and the demo panels still toggle via [hidden]. */
@media (prefers-reduced-motion: reduce) {
  .stl-how [data-rev],
  .stl-how [data-rev-child] { opacity: 1; transform: none; transition: none; }
  .stl-how__dots i,
  .stl-how__aurora i,
  .stl-mock,
  .stl-mock__sig--g,
  .stl-float { animation: none; }
  .stl-mock { transform: perspective(1400px) rotateY(-8deg) rotateX(3deg); }
  .is-rev .stl-twin__ticks i { animation: none; opacity: 1; }
  .stl-demo__result, .stl-demo__chip, .stl-sviz-sig.is-pick { animation: none; }
  .stl-step, .stl-how__sec { transition: none; }
}

/* The marker sits on its own line above the title rather than inline
 * beside it (Josh 2026-08-06). Inline, the marker was pinned to the top
 * of a title that is one line in column one and two lines in the other
 * two, so the three markers never shared an axis and the short column
 * carried dead space under its title. Stacking makes the lockup immune
 * to whatever the copy does later.
 *
 * The head stays a single grid item, so the subgrid rule above still
 * equalizes head heights across the three columns and the three
 * descriptions still share one baseline. */
.stl-whyband__head {
  display: block;
  /* The title owns the gap down to the description now. */
  margin-bottom: 0;
}
.stl-whyband__t {
  /* Block so it always breaks under the marker tile. */
  display: block;
  margin-bottom: 6px;
  font-family: var(--stl-font-display);
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.005em;
  color: var(--stl-text);
}
.stl-whyband__d {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--stl-text-muted);
}
/* Dismiss. Quiet until hovered — this is a control the user should be
 * able to find, not one the page should advertise. The band's 46px
 * right padding is sized to keep the third column's text clear of it. */
.stl-whyband__x {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 26px;
  height: 26px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--stl-text-subtle);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.stl-whyband__x:hover {
  background: var(--stl-surface-2);
  color: var(--stl-text);
  border-color: var(--stl-hairline);
}
.stl-whyband__x:focus-visible {
  outline: 2px solid var(--stl-rainbow-mid);
  outline-offset: 2px;
}
/* The way back. Lives in the picker card header and only appears once
 * the band has been dismissed. Text-weight on purpose: the picker
 * already has test tiles, the assessment entry and (when visible) the
 * band itself competing for the eye, and this is the least important
 * thing on the screen. */
.stl-whyband__reopen {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  width: auto;
  margin: 4px 0 2px;
  padding: 4px 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--stl-text-subtle);
  cursor: pointer;
  transition: color 0.15s ease;
}
/* Without this the link is visible ALL the time, band or no band. The
 * `display: inline-flex` above is an author-stylesheet rule and the
 * `hidden` attribute is only a UA-stylesheet `display: none`, so the
 * class wins and the attribute silently does nothing. Same companion
 * rule as .stl-header__nav-link[hidden] and friends elsewhere in this
 * file; any class here that sets `display` needs its own. */
.stl-whyband__reopen[hidden] { display: none; }
.stl-whyband__reopen:hover { color: var(--stl-rainbow-mid); }
.stl-whyband__reopen:focus-visible {
  outline: 2px solid var(--stl-rainbow-mid);
  outline-offset: 3px;
  border-radius: 4px;
}
/* A tinted tile, not a bare glyph. Gives the three columns anchors of
 * equal visual weight and makes the stoplight triplet read as one
 * deliberate mark instead of three loose dots. `flex` rather than
 * `inline-flex` so it is block-level inside the now-block head and the
 * title breaks cleanly beneath it. Both tile colors are theme tokens,
 * so light mode needs no second rule. */
.stl-why__mark {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  width: 28px;
  height: 28px;
  margin: 0 0 10px;
  border-radius: 9px;
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
}
/* Three dots at 5px with 2.5px gaps measure 20px, which centers inside
 * the 28px tile with 4px of clearance each side. At the old 6px/3px the
 * triplet crowded the tile wall. */
.stl-why__mark--signal { gap: 2.5px; }
.stl-why__mark--signal i {
  width: 5px; height: 5px; border-radius: 50%;
}
.stl-why__mark--signal i:nth-child(1) { background: #4ade80; }
.stl-why__mark--signal i:nth-child(2) { background: #facc15; }
.stl-why__mark--signal i:nth-child(3) { background: var(--stl-red); }
.stl-why__mark--desmos    { color: #4ade80; }
.stl-why__mark--worksheet { color: #fb923c; }

/* ── Front-page feature band (Mike 2026-07-25) ──────────────────────
 * The two highest-value tools — The Desmos Advantage lessons and the
 * custom worksheet builder — promoted to first-class tinted cards
 * under the test grid. Rich color identity (Desmos graph-green,
 * worksheet amber) so they read as FEATURES, not footnotes. */
.stl-featureband {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-3);
  margin-top: var(--s-4);
}
@media (max-width: 640px) {
  .stl-featureband { grid-template-columns: 1fr; }
}
.stl-featurecard {
  --fc-tint: 139, 134, 255;
  appearance: none; -webkit-appearance: none;
  font: inherit; color: var(--stl-text); text-align: left;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  padding: var(--s-4);
  border: 1px solid rgba(var(--fc-tint), 0.35);
  border-radius: var(--r-lg);
  background: linear-gradient(180deg, rgba(var(--fc-tint), 0.10), rgba(var(--fc-tint), 0.04));
  transition: transform 160ms var(--ease-out), border-color 160ms var(--ease-out), box-shadow 160ms var(--ease-out);
}
.stl-featurecard:hover {
  transform: translateY(-2px);
  border-color: rgba(var(--fc-tint), 0.65);
  /* Tinted glow in the card's own hue, negative spread — the shape every
   * other hover in this file uses. This was the only :hover in app.css
   * painting a plain black drop shadow, which read as a different
   * material from everything around it (Josh 2026-08-11). */
  box-shadow: 0 12px 28px -12px rgba(var(--fc-tint), 0.45);
}
/* Task hues (palette 2026-08-10): tasks own colors no TEST wears —
 * Desmos = deep blue (tool/ambience family), Worksheets = paper
 * neutral (it prints). Legacy: desmos green 74,222,128 (collided with
 * SAT), worksheet orange 251,146,60 (collided with SSAT). Light mode
 * darkens both so they survive a white canvas. */
.stl-featurecard--desmos    { --fc-tint: 59, 130, 246; }
.stl-featurecard--worksheet { --fc-tint: 231, 229, 228; }
:root[data-theme="light"] .stl-featurecard--desmos    { --fc-tint: 37, 99, 235; }
:root[data-theme="light"] .stl-featurecard--worksheet { --fc-tint: 87, 83, 78; }
.stl-featurecard__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px;
  border-radius: 12px;
  background: rgba(var(--fc-tint), 0.16);
  color: rgb(var(--fc-tint));
}
.stl-featurecard__title {
  font-weight: 700;
  font-size: 1.02rem;
  letter-spacing: 0.01em;
}
.stl-featurecard__sub {
  font-size: 0.86rem;
  color: var(--stl-text-muted);
  line-height: 1.45;
}
.stl-featurecard__cta {
  margin-top: auto;
  padding-top: 2px;
  font-size: 0.86rem;
  font-weight: 600;
  color: rgb(var(--fc-tint));
}

.stl-assess-entry {
  appearance: none; -webkit-appearance: none;
  font: inherit; color: inherit; text-align: left;
  cursor: pointer;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  margin-top: var(--s-4);
  padding: var(--s-4);
  border: 1px dashed var(--stl-hairline);
  border-radius: var(--r-lg);
  background: transparent;
  transition:
    border-color var(--dur-base) ease,
    background var(--dur-base) ease,
    transform var(--dur-fast) ease;
}
.stl-assess-entry:hover {
  border-color: var(--stl-accent);
  background: var(--stl-surface);
  transform: translateY(-1px);
}
.stl-assess-entry:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 3px;
}
.stl-assess-entry__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-md);
  border: 1px solid var(--stl-hairline);
  color: var(--stl-text-muted);
}
.stl-assess-entry__meta { display: grid; gap: 2px; min-width: 0; }
.stl-assess-entry__title { font-weight: 600; }
.stl-assess-entry__sub {
  color: var(--stl-text-muted);
  font-size: 13.5px;
  line-height: 1.45;
}
.stl-assess-entry__arrow { color: var(--stl-text-subtle); font-size: 18px; }

/* --lead: the first-timer state, sitting ABOVE the tiles as the
 * recommended first move (promoteAssessForNewcomers in app.js).
 *
 * The dashed border and transparent fill are what make this read as a
 * quiet sibling of the tiles — correct when it is a fallback below them,
 * wrong when it is the thing we are telling a new student to do. Solid
 * hairline and a real surface, so it reads as an offer rather than an
 * afterthought. It still does NOT out-shout the tiles: no accent border,
 * no glow. Choosing a test stays a legitimate alternative; we are
 * recommending, not blocking. */
/* --lead: the first-timer state, sitting ABOVE the tiles as the
 * recommended first move (promoteAssessForNewcomers in app.js).
 *
 * Rainbow border + rainbow glow + rainbow interior wash (Joshua
 * 2026-08-16). Every one of those comes from --stl-rainbow /
 * --stl-rainbow-soft rather than hardcoded colour, which matters more
 * than it looks: signal-theme.css overrides those tokens to the live
 * green→yellow→red signal ramp, and both have light-mode variants. Spell
 * the colours out here and this card is the one thing on the page that
 * ignores the theme.
 *
 * Three layers, deliberately separate:
 *   border  — the padding-box / border-box trick, identical to
 *             .stl-btn--pill-rainbow, so the rainbow rides the perimeter
 *             only and the interior stays ours to control.
 *   ::before — the interior wash. Same structural idea as the test
 *             tiles' ::before (135deg, fading out) so it reads as a
 *             sibling of them, but carrying the rainbow instead of one
 *             tile tint.
 *   ::after  — the outer glow. A blurred copy of the rainbow sitting
 *             behind the card.
 *
 * The glow CANNOT be a box-shadow (a shadow takes one colour, not a
 * gradient) and the card therefore cannot take overflow:hidden the way
 * the tiles do, or the glow would be clipped off. That is why ::before
 * carries its own border-radius instead of relying on the parent to
 * clip it.
 *
 * The blurred ::after deliberately carries NO box-shadow: Safari clips
 * box-shadow on elements that also have a filter, and Josh is on Safari. */
.stl-assess-entry--lead {
  position: relative;
  /* No `isolation: isolate` here, deliberately. It would make this card
   * a stacking context, and a negative-z child cannot paint behind the
   * background of the element that forms its context — the glow would
   * come out as a blurry haze ON the card instead of around it. */
  margin-top: 0;
  margin-bottom: var(--s-4);
  border: 1.5px solid transparent;
  background:
    linear-gradient(var(--stl-bg), var(--stl-bg)) padding-box,
    var(--stl-rainbow) border-box;
}
/* Interior wash — the tiles' treatment with the rainbow in place of a
 * single tint. Fades out by 70% so the text side of the card stays
 * quiet and legible rather than sitting on full chroma. */
.stl-assess-entry--lead::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--stl-rainbow-soft);
  -webkit-mask-image: linear-gradient(135deg, #000 0%, rgba(0, 0, 0, 0.62) 55%, transparent 100%);
  mask-image: linear-gradient(135deg, #000 0%, rgba(0, 0, 0, 0.62) 55%, transparent 100%);
  /* No z-index, matching .stl-picker__tile::before. The wash is faint
   * enough (0.16 alpha, masked down from there) that painting over the
   * label costs nothing, and staying out of the z-index game keeps it
   * from interacting with the glow below. */
  pointer-events: none;
}
/* Outer glow — three coloured box-shadows, NOT a blurred pseudo-element.
 *
 * The first version was a ::after carrying var(--stl-rainbow) with
 * filter: blur(), sitting at z-index: -1. It looked right in Chrome and
 * was broken in Safari (Josh, 2026-08-16): the negative-z pseudo painted
 * ON TOP of the card instead of behind it, so the whole card was flooded
 * with a blurry full-saturation rainbow and the hover — which raised the
 * glow's opacity — made it worse.
 *
 * The lesson is not "Safari is wrong", it is that negative z-index only
 * lands behind the parent's background when nothing in the ancestor chain
 * has quietly opened a stacking context, and that is not a property this
 * card can guarantee about wherever it gets used.
 *
 * A box-shadow cannot have this problem. It is painted outside the border
 * box, underneath the element, by definition — no stacking context, no
 * z-index, no pseudo-element. Three of them at the signal hues give the
 * rainbow spread; a single gradient shadow does not exist in CSS.
 *
 * Also safe on the Safari filter/box-shadow clipping bug, because the card
 * itself carries no filter. */
.stl-assess-entry--lead {
  box-shadow:
    -14px 8px 30px -14px rgba(var(--stl-green-rgb), 0.55),
      0px 10px 32px -14px rgba(var(--stl-yellow-rgb), 0.50),
     14px 8px 30px -14px rgba(var(--stl-red-rgb), 0.55);
  transition:
    box-shadow var(--dur-base) var(--ease-out),
    transform var(--dur-fast) var(--ease-spring);
}
.stl-assess-entry--lead:hover {
  /* The base :hover paints an accent border and a flat surface, which
   * would wipe both the rainbow border and the wash. Restore them and
   * let the glow do the hover instead. */
  border-color: transparent;
  background:
    linear-gradient(var(--stl-bg), var(--stl-bg)) padding-box,
    var(--stl-rainbow) border-box;
  box-shadow:
    -16px 10px 36px -13px rgba(var(--stl-green-rgb), 0.7),
      0px 12px 38px -13px rgba(var(--stl-yellow-rgb), 0.62),
     16px 10px 36px -13px rgba(var(--stl-red-rgb), 0.7);
  transform: translateY(-1px);
}
.stl-assess-entry--lead .stl-assess-entry__icon {
  border-color: var(--stl-hairline-hi);
  color: var(--stl-text);
}
@media (prefers-reduced-motion: reduce) {
  .stl-assess-entry--lead,
  .stl-assess-entry--lead:hover { transition: none; transform: none; }
}

/* Scope tiles on the assess screen are radios, not launch shortcuts —
 * the selected one carries a persistent tinted ring. */
.stl-assess__grid { margin-top: var(--s-2); }
.stl-assess__tile.is-active {
  /* FIXLIST P2 (2026-08-18): dropped the 1px inset ring — rule 2, selected
   * gets lighter never thicker. The border colour + diffuse glow carry it. */
  border-color: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.75);
  background: rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.10);
  box-shadow: 0 12px 28px -16px rgba(var(--tile-tint, var(--stl-rainbow-mid-rgb)), 0.4);
}
.stl-assess__tile.is-active::before { opacity: 1; }
.stl-assess .stl-score__section { margin-top: var(--s-5); }
.stl-assess .stl-radar__cta { margin-top: var(--s-5); }

/* Results banner — reuses the shared-banner shell; the dot goes accent
 * so it reads "context", not "warning". */
.stl-assess-banner .stl-shared-banner__dot { background: var(--stl-accent); }
.stl-assess-banner strong { font-weight: 650; }

/* History row chip marking an attempt as a baseline assessment. Same
 * register as the synthetic chip, accent-toned. */
.stl-history__assess {
  font-style: normal;
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: var(--r-pill);
  background: rgba(var(--stl-rainbow-mid-rgb), 0.10);
  color: var(--stl-accent);
  font-size: 9.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* ===== "Get familiar with Desmos" section (Mike build spec 2026-07) ===== */
.stl-learn { display: grid; gap: var(--s-4); }
.stl-learn > div { display: grid; gap: var(--s-4); }
/* Lesson index cards */
.stl-learn__list { display: grid; gap: 8px; }
.stl-learn__card {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14px;
  width: 100%; text-align: left; padding: 14px 16px;
  border: 1px solid var(--stl-hairline); border-radius: var(--r-md);
  background: var(--stl-surface); color: var(--stl-text); font: inherit; cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.stl-learn__card:hover { background: var(--stl-surface-2); border-color: var(--stl-accent-line); }
.stl-learn__card-num {
  width: 32px; height: 32px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--stl-accent-line); border-radius: 50%;
  color: var(--stl-accent); font-weight: 650; font-variant-numeric: tabular-nums;
}
.stl-learn__card-meta { display: grid; gap: 2px; min-width: 0; }
.stl-learn__card-title { font-weight: 600; letter-spacing: -0.01em; }
.stl-learn__card-hook { color: var(--stl-text-muted); font-size: 13px; }
.stl-learn__card-arrow { color: var(--stl-text-subtle); transition: transform 120ms ease, color 120ms ease; }
.stl-learn__card:hover .stl-learn__card-arrow { transform: translateX(2px); color: var(--stl-text); }
/* Cheat sheet */
.stl-learn__cheat { display: grid; gap: 10px; margin-top: var(--s-3); }
.stl-learn__cheat .stl-h2 { margin: 0; }
.stl-learn__cheat-sub { margin: 0; }
.stl-learn__cheat-table {
  display: grid; gap: 0;
  border: 1px solid var(--stl-hairline); border-radius: var(--r-md); overflow: hidden;
}
.stl-learn__cheat-row {
  display: grid; grid-template-columns: 28px minmax(0, 1fr) minmax(0, 1.1fr); align-items: baseline; gap: 12px;
  padding: 10px 14px; cursor: pointer;
  background: var(--stl-surface);
  transition: background 120ms ease;
}
.stl-learn__cheat-row + .stl-learn__cheat-row { border-top: 1px solid var(--stl-hairline); }
.stl-learn__cheat-row:hover { background: var(--stl-surface-2); }
.stl-learn__cheat-num { color: var(--stl-accent); font-weight: 650; font-variant-numeric: tabular-nums; }
.stl-learn__cheat-asks { font-size: 13.5px; color: var(--stl-text); }
.stl-learn__cheat-move { font-size: 13px; color: var(--stl-text-muted); }
/* Lesson view */
.stl-learn__crumb { justify-self: start; }
.stl-learn__head { display: grid; gap: 6px; }
.stl-learn__head .stl-h1 { margin: 0; }
.stl-learn__hook { margin: 0; font-style: italic; }
.stl-learn__steps { margin: 0; padding-left: 1.4em; display: grid; gap: 10px; }
.stl-learn__steps li { padding-left: 4px; }
.stl-learn__steps li::marker { color: var(--stl-accent); font-weight: 650; }
/* Calculator frame */
.stl-learn__calcwrap {
  border: 1px solid var(--stl-hairline); border-radius: var(--r-md);
  overflow: hidden; background: var(--stl-surface);
}
.stl-learn__calc, #learn-calc-host { width: 100%; height: clamp(300px, 45vh, 420px); }
/* Note callouts */
.stl-learn__notes { display: grid; gap: 8px; }
.stl-learn__note {
  display: grid; gap: 4px; padding: 12px 14px;
  border: 1px solid var(--stl-hairline); border-left-width: 3px; border-radius: var(--r-md);
  background: var(--stl-surface); font-size: 14px;
}
.stl-learn__note-k {
  font-family: var(--stl-font-mono);
  font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--stl-text-muted);
}
.stl-learn__note--works { border-left-color: var(--stl-accent); }
.stl-learn__note--key   { border-left-color: var(--stl-accent); }
.stl-learn__note--key .stl-learn__note-k { color: var(--stl-accent); }
.stl-learn__note--bonus { border-left-color: var(--stl-green); }
.stl-learn__note--bonus .stl-learn__note-k { color: #4ade80; }
.stl-learn__note--skip  { border-left-color: var(--stl-yellow); }
.stl-learn__note--skip .stl-learn__note-k { color: var(--stl-yellow); }
.stl-learn__note--head  { border-left-color: var(--stl-yellow); }
.stl-learn__note--head .stl-learn__note-k { color: var(--stl-yellow); }
/* Lesson prev/next */
.stl-learn__nav { display: flex; justify-content: space-between; gap: 12px; }
.stl-learn__nav-btn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stl-learn__nav-btn:disabled { opacity: 0.35; cursor: default; }
/* Practice sub-view */
.stl-learn__q { display: grid; gap: var(--s-3); }
.stl-learn__q-meta { margin: 0; }
.stl-learn__q-stem { font-size: 1.05rem; line-height: 1.6; }
.stl-learn__choices { display: grid; gap: 8px; }
.stl-learn__choice {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 12px;
  width: 100%; text-align: left; padding: 12px 14px;
  border: 1px solid var(--stl-hairline); border-radius: var(--r-md);
  background: var(--stl-surface); color: var(--stl-text); font: inherit; cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.stl-learn__choice:hover:not(:disabled) { background: var(--stl-surface-2); border-color: var(--stl-accent-line); }
.stl-learn__choice:disabled { cursor: default; }
.stl-learn__choice-letter {
  width: 26px; height: 26px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--stl-hairline-hi); border-radius: 50%;
  font-size: 12.5px; font-weight: 650;
}
.stl-learn__choice.is-correct {
  border-color: rgba(var(--stl-green-rgb), 0.6); background: rgba(var(--stl-green-rgb), 0.10);
}
.stl-learn__choice.is-correct .stl-learn__choice-letter { border-color: var(--stl-green); color: #4ade80; }
.stl-learn__choice.is-wrong {
  border-color: rgba(var(--stl-red-rgb), 0.55); background: rgba(var(--stl-red-rgb), 0.10);
}
.stl-learn__choice.is-wrong .stl-learn__choice-letter { border-color: var(--stl-red); color: var(--stl-red); }
.stl-learn__gridin { display: flex; gap: 8px; align-items: stretch; }
/* Harden the hide (Mike 2026-07-25): the author display:flex above
 * beats the UA [hidden]{display:none}, so the type-an-answer box was
 * showing UNDER the multiple-choice options too. It belongs only on
 * grid-in questions (no choices) — same bug class as the quiz-tools
 * hardening above. */
.stl-learn__gridin[hidden] { display: none !important; }
.stl-learn__gridin .stl-input { flex: 1; min-width: 0; }
.stl-learn__verdict { margin: 0; font-weight: 600; }
.stl-learn__verdict.is-right { color: #4ade80; }
.stl-learn__verdict.is-wrong { color: #f0b429; }
.stl-learn__explanation {
  padding: 14px 16px; border: 1px solid var(--stl-hairline); border-radius: var(--r-md);
  background: var(--stl-surface-2); font-size: 14.5px; line-height: 1.6;
}
.stl-learn__q-figure[hidden] { display: none; }
/* Light-mode tweaks */
:root[data-theme="light"] .stl-learn__note--bonus .stl-learn__note-k { color: #15803d; }
:root[data-theme="light"] .stl-learn__note--skip .stl-learn__note-k { color: #a16207; }
:root[data-theme="light"] .stl-learn__note--head .stl-learn__note-k { color: #a16207; }
:root[data-theme="light"] .stl-learn__verdict.is-right { color: #15803d; }
:root[data-theme="light"] .stl-learn__verdict.is-wrong { color: #a16207; }
/* Mobile */
@media (max-width: 640px) {
  .stl-learn__cheat-row { grid-template-columns: 24px minmax(0, 1fr); }
  .stl-learn__cheat-move { grid-column: 2; }
  .stl-learn__calc, #learn-calc-host { height: clamp(260px, 50vh, 360px); }
}

/* ===== FR-4: test-length preset cards (10 Warm-up / 22 One module /
 * 44 Full math). Center the icon + number + label stack; the icon is
 * accent-toned and the label reuses the .stl-tier__range slot. ===== */
.stl-tier--length {
  align-content: center;
  justify-items: center;
  text-align: center;
  gap: 2px;
  min-height: 76px;
}
.stl-tier--length .stl-tier__icon { display: inline-flex; color: var(--stl-accent); margin-bottom: 2px; }
.stl-tier--length .stl-tier__icon svg { display: block; }
.stl-tier--length .stl-tier__name { font-size: 1.25rem; }
.stl-tier--length .stl-tier__range {
  text-transform: uppercase; letter-spacing: 0.07em; font-size: 10px;
}
@media (max-width: 640px) {
  /* Keep the preset cards as a compact 2-up card grid on phones —
   * override the generic .stl-tier pill collapse (same approach as
   * the teacher composer tiers). */
  #score-length { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  #score-length .stl-tier--length {
    display: grid; border-radius: var(--r-md); background: var(--stl-surface);
    min-height: 64px; padding: 10px 12px;
  }
  #score-length .stl-tier--length .stl-tier__name { font-size: 1.1rem; font-family: var(--stl-font-display); font-weight: 600; }
  #score-length .stl-tier--length .stl-tier__range { font-size: 9.5px; }
}

/* Learn sub-view layout: the inner wrappers are grids, so their
 * [hidden] state must explicitly re-collapse them (display:grid would
 * otherwise defeat the hidden attribute). */
#learn-lesson-body, #learn-practice { display: grid; gap: var(--s-4); }
#learn-index[hidden], #learn-lesson[hidden],
#learn-lesson-body[hidden], #learn-practice[hidden] { display: none; }

/* ============================================================
 * Worksheet builder (teacher shell → Worksheets tab)
 * Builder grid + summary sidebar on screen; the printable sheet
 * reuses the stl-print system (additions at the bottom of the
 * @media print block's companion rules below).
 * ============================================================ */

.stl-wk[hidden], .stl-wk__sheet[hidden] { display: none; }
.stl-wk {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 280px;
  gap: 20px;
  align-items: start;
}
@media (max-width: 900px) {
  .stl-wk { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  .stl-wk__mhead .stl-wk__bulkcells { grid-template-columns: 1fr 1fr; }
}
.stl-wk__main { min-width: 0; }

/* ================= stl-msdd — multi-select dropdown =================
 * Pill trigger + checklist panel. Picked from three explored designs
 * (Josh 2026-08-07) because the trigger reuses the pill vocabulary
 * already on the page, so a row of these reads as the same family of
 * controls the builder always had. Namespaced away from .stl-wk on
 * purpose: the admin dashboard's filters adopt this next.
 *
 * The trigger label NEVER changes — a count chip appears on the right
 * only while the dropdown is narrowing something, so a resting row is
 * quiet and a working row is obvious. */
.stl-msdd { position: relative; display: inline-block; }
.stl-msdd__trigger {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 14px 9px 15px; cursor: pointer;
  background: var(--stl-surface);
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-pill);
  color: var(--stl-text); font: inherit; font-size: .9rem; font-weight: 550;
  white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, transform .05s ease;
}
.stl-msdd__trigger:hover { background: var(--stl-surface-2); border-color: var(--stl-text-faint); }
.stl-msdd__trigger:active { transform: translateY(1px); }
.stl-msdd__count {
  display: inline-grid; place-items: center; min-width: 19px; height: 19px; padding: 0 6px;
  background: var(--stl-accent-soft); color: var(--stl-accent);
  border-radius: var(--r-pill);
  font-size: 11.5px; font-weight: 700; font-variant-numeric: tabular-nums;
}
/* A "value" chip states what the control is set to rather than how much
 * it is narrowing — used by single-select dropdowns like Test type,
 * which always have a value and so must not read as permanently active.
 * Muted, and excluded from the tinted-border rule below. */
.stl-msdd__count--value {
  background: var(--stl-surface-3);
  color: var(--stl-text-muted);
  font-weight: 600;
}
/* A dropdown carrying a count is doing work — tint its border so the
 * active ones are findable without reading every chip. */
.stl-msdd:has(.stl-msdd__count:not(.stl-msdd__count--value)) .stl-msdd__trigger {
  border-color: var(--stl-accent-line);
  background: var(--stl-accent-soft);
}
/* Central-icon-system chevron (lucide chevron-down, 24 viewBox) — the
 * one caret every dropdown on the site wears (Josh 2026-08-11). */
.stl-msdd__caret {
  width: 13px; height: 13px; flex: none; opacity: .55;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  transition: transform .16s ease, opacity .16s ease;
}
.stl-msdd.is-open .stl-msdd__caret { transform: rotate(180deg); opacity: .9; }

.stl-msdd__panel {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 40; min-width: 250px;
  /* OPAQUE, not var(--stl-surface): this floats over the topic grid and
   * a translucent panel would let the cells read through it. Uses the
   * shared floating-panel token since 2026-08-18 — which also fixed its
   * black-dropdown-on-white-page light-mode bug. */
  background: var(--stl-panel);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-lg);
  box-shadow: 0 24px 60px -18px rgba(0, 0, 0, .85), inset 0 1px 0 rgba(255, 255, 255, .03);
  padding: 7px;
  max-height: min(60vh, 420px); overflow-y: auto; overscroll-behavior: contain;
  opacity: 0; transform: translateY(-5px) scale(.985); pointer-events: none;
  transition: opacity .14s ease, transform .14s cubic-bezier(.34, 1.4, .64, 1);
}
.stl-msdd.is-open .stl-msdd__panel { opacity: 1; transform: none; pointer-events: auto; }
/* Near the right edge the panel would otherwise run off the page. */
.stl-msdd:last-child .stl-msdd__panel { left: auto; right: 0; }
.stl-msdd__head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 5px 9px 8px; margin-bottom: 3px;
  border-bottom: 1px solid var(--stl-hairline);
}
.stl-msdd__head b {
  font-size: 11.5px; letter-spacing: .11em; text-transform: uppercase;
  color: var(--stl-text-subtle); font-weight: 600;
}
.stl-msdd__clear {
  background: none; border: 0; color: var(--stl-text-subtle);
  font: inherit; font-size: 12px; font-weight: 550;
  padding: 2px 7px; border-radius: 6px; cursor: pointer;
  transition: color .12s, background .12s;
}
.stl-msdd__clear:hover { color: var(--stl-text); background: var(--stl-surface-2); }
.stl-msdd__row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 9px; border-radius: var(--r-sm);
  background: none; border: 0; color: var(--stl-text);
  font: inherit; font-size: .9rem; cursor: pointer; text-align: left;
  transition: background .12s ease;
}
.stl-msdd__row:hover:not([disabled]) { background: var(--stl-surface-2); }
.stl-msdd__row[disabled] { color: var(--stl-text-faint); cursor: default; }
.stl-msdd__box {
  width: 16px; height: 16px; flex: none; border-radius: 5px;
  border: 1.5px solid var(--stl-hairline-hi); background: var(--stl-surface);
  display: grid; place-items: center;
  transition: background .13s ease, border-color .13s ease;
}
.stl-msdd__box svg {
  width: 10px; height: 10px; opacity: 0; transform: scale(.6);
  fill: none; stroke: var(--stl-bg-deep); stroke-width: 3.2;
  stroke-linecap: round; stroke-linejoin: round;
  transition: opacity .13s ease, transform .13s cubic-bezier(.34, 1.4, .64, 1);
}
.stl-msdd__row.is-on .stl-msdd__box { background: var(--stl-accent); border-color: var(--stl-accent); }
.stl-msdd__row.is-on .stl-msdd__box svg { opacity: 1; transform: none; }
.stl-msdd__lbl { min-width: 0; }
.stl-msdd__cnt {
  margin-left: auto; flex: none;
  color: var(--stl-text-faint); font-size: 12px; font-variant-numeric: tabular-nums;
}
.stl-msdd__dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
/* Difficulty tiers and frequency bands both use the dot, and both reuse
 * the palette their old pills wore. */
.stl-msdd__dot--easy,   .stl-msdd__dot--high { background: var(--stl-green); }
.stl-msdd__dot--medium, .stl-msdd__dot--med  { background: var(--stl-yellow); }
.stl-msdd__dot--hard,   .stl-msdd__dot--low  { background: var(--stl-red); }
.stl-msdd__dot--insane { background: var(--stl-accent); }

/* ---- Option A toolbar (Josh 2026-08-11) — one row holds the test
 * type chips, search, both filter dropdowns, and Clear. ---- */
.stl-wk__toolbar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 10px 12px; margin: 0 0 16px;
  border: 1px solid var(--stl-hairline);
  border-radius: 12px;
  background: var(--stl-surface);
}
.stl-wk__typechips { display: flex; gap: 6px; flex: none; }
.stl-wk__toolbar .stl-wk__search { flex: 1; min-width: 160px; }
.stl-wk__filtergroup { display: flex; flex-wrap: wrap; gap: 6px; }

.stl-wk__search { min-width: 0; }

/* Option A (Josh 2026-08-11): the all-tier toggles ARE the matrix's
 * column headers now — compact mono buttons, one per tier column.
 * They keep the filled-pill affordance pass below (clickable things
 * look clickable, per Mike 2026-07-26). */
.stl-wk__bulk {
  font-family: var(--stl-font-mono);
  font-size: 10px; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase;
  padding: 5px 0; text-align: center;
  border-radius: 8px;
  background: transparent; cursor: pointer;
}
.stl-wk__bulk[disabled] { opacity: 0.35; cursor: default; }

/* ── Clickability affordance pass (Mike 2026-07-26) ──────────────────
 * In the builder, everything clickable is a FILLED pill with a real
 * border and a hover lift; everything informational is borderless
 * dot-key text. Rest state must already read as "button" — before
 * this, inactive filter chips were transparent and looked like labels
 * while the (non-clickable) legend pills looked like buttons. */
#wk-builder .stl-chip,
#wk-builder .stl-wk__bulk {
  background: var(--stl-surface-2);
  border: 1px solid var(--stl-hairline-hi);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
}
/* Selected chips must still WIN over the rest-state fill above — the
 * ID selector would otherwise flatten the accent/tint treatment. */
#wk-builder .stl-chip.is-active {
  border-color: var(--stl-accent);
  background: var(--stl-accent-soft);
}
#wk-builder .stl-chip.is-active[style*="--chip-tint"] {
  border-color: rgb(var(--chip-tint));
  background: rgba(var(--chip-tint), 0.16);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
}
#wk-builder .stl-chip:hover:not(:disabled),
#wk-builder .stl-wk__bulk:hover:not([disabled]) {
  border-color: var(--stl-text-faint);
  transform: translateY(-1px);
}
#wk-builder .stl-chip:active:not(:disabled),
#wk-builder .stl-wk__bulk:active:not([disabled]) { transform: translateY(0); }
#wk-builder .stl-wk__bulk[disabled] { box-shadow: none; background: transparent; }

/* Per-topic CARDS inside the big container (Josh 2026-08-11, round 2:
 * the stacked label-over-buttons card from the pre-matrix builder read
 * better than the two-track row — so each topic keeps its own bordered
 * card, and all of them nest inside the one outer matrix shell). The
 * column-header bulk toggles stay, padded to line up with the cards'
 * interior button grid (12px shell + 1px card border + 14px card
 * padding = 27px). */
.stl-wk__matrix {
  border: 1px solid var(--stl-hairline);
  border-radius: 14px;
  background: var(--stl-surface);
  padding: 12px;
}
.stl-wk__mhead {
  display: block;
  margin: 0 -12px 12px;
  padding: 2px 27px 12px;
  border-bottom: 1px solid var(--stl-hairline);
}
.stl-wk__mhead-lbl { display: none; }
.stl-wk__mhead .stl-wk__bulkcells {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px;
}
.stl-wk__domhead {
  display: flex; align-items: baseline; gap: 8px;
  padding: 10px 3px 2px;
}
.stl-wk__domhead span {
  font-family: var(--stl-font-mono);
  font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--stl-text-subtle);
}
.stl-wk__domhead em { font-style: normal; font-size: 11px; color: var(--stl-text-subtle); }
/* Legend lives in the sidebar under Generate (Josh 2026-08-11). */
.stl-wk__legendline {
  margin: 2px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--stl-hairline);
  font-size: 12px; color: var(--stl-text-subtle);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.stl-wk__legendline b { width: 100%; }
.stl-wk__legendline b { color: var(--stl-text-muted); font-weight: 600; }
.stl-wk__grid { display: flex; flex-direction: column; gap: 10px; }
.stl-wk__none { padding: 18px 4px; }
.stl-wk__row {
  display: grid;
  gap: 11px;
  padding: 14px;
  border: 1px solid var(--stl-hairline);
  border-radius: 12px;
  background: var(--stl-bg);
  transition: border-color var(--dur-base) var(--ease-spring);
}
.stl-wk__row:hover { border-color: var(--stl-hairline-hi); }
.stl-wk__cells { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
@media (max-width: 420px) {
  .stl-wk__cells { grid-template-columns: 1fr 1fr; }
}
.stl-wk__tname { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex-wrap: wrap; }
.stl-wk__tlabel { font-size: 13.5px; font-weight: 500; color: var(--stl-text); }
.stl-wk__dom {
  font-family: var(--stl-font-mono);
  font-size: 10px; letter-spacing: 0.05em;
  color: var(--stl-text-faint); text-transform: uppercase;
}
.stl-wk__freq {
  font-family: var(--stl-font-mono);
  font-size: 10px; font-weight: 600; letter-spacing: 0.04em;
  padding: 2px 7px; border-radius: var(--r-pill);
}
/* Frequency chips — restored to the original standalone builder's
 * BLUES (Mike 2026-07-25): High = solid, Medium = soft, Low = outlined.
 * Retoned onto the --stl-blue token 2026-08-18 (FIXLIST P4) — the six
 * bespoke hexes were the only palette in the app with no token and no
 * light-theme story; composing from --stl-blue-rgb keeps Mike's
 * solid/soft/outline hierarchy and flips with the theme for free. */
.stl-wk__freq--high { background: var(--stl-blue); color: var(--stl-bg-deep); }
.stl-wk__freq--med  { background: rgba(var(--stl-blue-rgb), 0.22); color: var(--stl-blue-text); }
.stl-wk__freq--low  { background: transparent; color: var(--stl-blue); border: 1px solid rgba(var(--stl-blue-rgb), 0.45); }
/* Dot-key frequency badges (Mike 2026-07-26): NOT pill-shaped — in the
 * builder, pills mean "clickable". Same blues, carried by the dot;
 * Low's dot is an outlined ring, matching the old outlined pill. */
.stl-wk__freqkey {
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--stl-font-mono);
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.04em;
  color: var(--stl-text-subtle);
}
.stl-wk__freqkey::before {
  content: ''; width: 8px; height: 8px; border-radius: 50%; flex: none;
}
.stl-wk__freqkey--high::before { background: var(--stl-blue); box-shadow: 0 0 4px rgba(var(--stl-blue-rgb), .6); }
.stl-wk__freqkey--med::before  { background: rgba(var(--stl-blue-rgb), 0.35); }
.stl-wk__freqkey--low::before  { background: transparent; border: 1.5px solid rgba(var(--stl-blue-rgb), 0.45); }

/* Frequency legend row (left of the All-Easy…All-Insane bulk buttons). */
/* (legend container retired 2026-08-11 — keys live in .stl-wk__legendline) */


/* Tier cells — hollow chip w/ tier hue; is-on fills the tint. */
.stl-wk__cell {
  height: 44px;
  border-radius: 10px;
  border: 1px solid var(--stl-hairline);
  background: transparent;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
}
/* Labels back in the cells (Josh 2026-08-11) — tier text in every
 * swatch; the selected state stays the filled background. */
.stl-wk__cell-tier { font-size: 14px; font-weight: 600; }
.stl-wk__cell--empty { border-color: transparent; cursor: default; }
.stl-wk__cell-count {
  font-family: var(--stl-font-mono);
  font-size: 10.5px; font-weight: 500;
  color: var(--stl-text-subtle);
  font-variant-numeric: tabular-nums;
}
.stl-wk__cell--easy,   .stl-wk__bulk.stl-wk__cell--easy   { color: var(--stl-green-text); border-color: rgba(var(--stl-green-rgb), 0.35); }
.stl-wk__cell--medium, .stl-wk__bulk.stl-wk__cell--medium { color: var(--stl-yellow-text); border-color: rgba(var(--stl-yellow-rgb), 0.32); }
.stl-wk__cell--hard,   .stl-wk__bulk.stl-wk__cell--hard   { color: var(--stl-red-text); border-color: rgba(var(--stl-red-rgb), 0.35); }
.stl-wk__cell--insane, .stl-wk__bulk.stl-wk__cell--insane { color: var(--stl-accent); border-color: var(--stl-accent-line); }
.stl-wk__cell--easy.is-on   { background: rgba(var(--stl-green-rgb), 0.14); border-color: var(--stl-green); }
.stl-wk__cell--medium.is-on { background: rgba(var(--stl-yellow-rgb), 0.12); border-color: var(--stl-yellow); }
.stl-wk__cell--hard.is-on   { background: rgba(var(--stl-red-rgb), 0.14); border-color: var(--stl-red); }
.stl-wk__cell--insane.is-on { background: var(--stl-accent-soft); border-color: var(--stl-accent); }

/* Summary sidebar */
.stl-wk__side {
  /* 84px = the 60px header + a 24px breathing gap. The header grows by
   * the iOS safe-area inset (see .stl-header), so this offset has to
   * grow with it or the panel tucks under the bar on a notched phone.
   * Sticky only works at all because <body> uses `overflow-x: clip`
   * rather than `hidden` — see the note on .stl-app. */
  position: sticky;
  top: calc(84px + env(safe-area-inset-top));
  /* A tall panel (many checkboxes, a long cell list) would otherwise
   * run past the bottom of the viewport and strand the Generate button
   * where it can never be scrolled to, because a sticky element pinned
   * at the top does not scroll with the page. Cap it to what actually
   * fits and let the panel scroll internally instead. */
  max-height: calc(100dvh - 84px - env(safe-area-inset-top) - 20px);
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 12px;
  padding: 16px;
  border: 1px solid var(--stl-hairline);
  border-radius: 14px;
  background: var(--stl-surface);
}
/* Single-column layout: the panel sits below the grid in source order,
 * so pinning it to the top of the viewport would park it over the
 * content the user is trying to read. Let it flow normally. */
@media (max-width: 900px) {
  .stl-wk__side {
    position: static;
    max-height: none;
    overflow-y: visible;
  }
}
.stl-wk__stats { display: flex; gap: 16px; }
/* Option A: the two numbers become small stat cards (2-up). */
.stl-wk__stats--cards { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.stl-wk__stats--cards .stl-wk__stat {
  padding: 10px;
  border: 1px solid var(--stl-hairline);
  border-radius: 10px;
  background: var(--stl-surface-2);
}
.stl-wk__stat { display: flex; flex-direction: column; }
.stl-wk__stat strong {
  font-family: var(--stl-font-display);
  font-size: 22px; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.stl-wk__stat span { font-size: 11.5px; color: var(--stl-text-subtle); }
.stl-check {
  display: flex; align-items: flex-start; gap: 8px;
  font-size: 13px; color: var(--stl-text-muted);
  cursor: pointer; user-select: none;
}
.stl-check input { accent-color: var(--stl-accent); margin-top: 2px; }
.stl-check .stl-hint { font-style: normal; }

/* On-screen sheet preview — a paper card on the dark canvas. */
.stl-wk__sheetbar { display: flex; justify-content: space-between; gap: 10px; margin-bottom: 14px; }
.stl-wk__sheetbar-right { display: flex; gap: 8px; }
.stl-wkpaper {
  background: #fff;
  color: #000;
  border-radius: 6px;
  padding: 42px 48px;
  /* Deliberately a literal, not var(--shell-read). This is a sheet of
   * paper, not a page column — it happens to be 760px today, but it
   * should not follow the app's reading width if that ever moves. */
  max-width: 760px;
  margin: 0 auto;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
  font-family: Georgia, "Source Serif 4", "Iowan Old Style", serif;
  font-size: 15px;
  line-height: 1.5;
}
@media (max-width: 700px) { .stl-wkpaper { padding: 24px 20px; } }

/* Print-vocabulary rules that must ALSO apply on screen inside the
 * preview card (the @media print versions only exist while printing). */
.stl-wkpaper .stl-print-head { border-bottom: 1px solid #999; padding-bottom: 12px; margin-bottom: 16px; }
.stl-wkpaper .stl-print-head h1 {
  font-family: var(--stl-font-body);
  font-size: 24px; font-weight: 600; margin: 0 0 4px; letter-spacing: -0.02em; color: #000;
}
.stl-wkpaper .stl-print-head__meta {
  font-family: var(--stl-font-mono);
  font-size: 11px; color: #555; margin: 0; letter-spacing: 0.04em;
}
.stl-wkpaper .stl-print-q { margin: 0 0 18px; padding: 0 0 14px; border-bottom: 1px solid #ddd; }
.stl-wkpaper .stl-print-q__head {
  font-family: var(--stl-font-mono);
  font-size: 11px; color: #666; margin: 0 0 6px; letter-spacing: 0.05em; text-transform: uppercase;
}
.stl-wkpaper .stl-print-q__num {
  color: #000; font-weight: 600; margin-right: 8px;
  font-family: var(--stl-font-body); font-size: 15px; letter-spacing: 0; text-transform: none;
}
.stl-wkpaper .stl-print-q__stem { margin: 0 0 10px; color: #000; }
.stl-wkpaper .stl-print-q__choices { list-style: none; margin: 0; padding: 0 0 0 18px; }
.stl-wkpaper .stl-print-q__choice { margin: 0 0 4px; color: #000; }
.stl-wkpaper .stl-print-q__letter { font-weight: 600; margin-right: 5px; font-family: var(--stl-font-mono); font-size: 13px; }
.stl-wkpaper .stl-print-q__figure { margin: 6px 0 10px; }
.stl-wkpaper .stl-print-q__figure img, .stl-wkpaper .stl-print-q__figure svg { max-width: 80%; height: auto; color: #000; }
.stl-wkpaper .stl-print-q__figure table { border-collapse: collapse; font-size: 13px; font-family: var(--stl-font-body); }
.stl-wkpaper .stl-print-q__figure th, .stl-wkpaper .stl-print-q__figure td { border: 1px solid #999; padding: 4px 10px; text-align: right; }
.stl-wkpaper .stl-print-q__figure th:first-child, .stl-wkpaper .stl-print-q__figure td:first-child { text-align: left; }
.stl-wkpaper .stl-print-q__figure th { background: #eee; font-weight: 600; }
.stl-wkpaper .stl-print-q__expl {
  margin: 6px 0 10px; padding: 4px 0 4px 12px;
  border-left: 2px solid #aaa; font-style: italic; color: #333; font-size: 13px;
}
.stl-wkpaper .stl-quiz__var { font-style: italic; color: #000; }
.stl-wkpaper .stl-math-fracsup, .stl-wkpaper .stl-math-sup { color: #000; }

/* Shared (screen preview AND print) worksheet-only vocabulary. */
.stl-print-nameline {
  display: flex; gap: 28px; flex-wrap: wrap;
  margin: 0 0 22px; font-size: 14px; color: #000;
}
.stl-print-nameline__f { display: inline-flex; align-items: baseline; gap: 8px; }
.stl-print-q__blank {
  display: inline-block; min-width: 180px;
  border-bottom: 1px solid #333; height: 1em;
}
.stl-print-q__blank--sm { min-width: 90px; }
.stl-print-q__sprline { margin: 4px 0 0; padding-left: 18px; color: #000; }
.stl-print-key { margin-top: 26px; padding-top: 16px; border-top: 2px solid #000; }
.stl-print-key h2 {
  font-family: var(--stl-font-body);
  font-size: 17px; font-weight: 600; margin: 0 0 10px; color: #000;
}
.stl-print-key__grid {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px 18px;
  font-variant-numeric: tabular-nums;
}
@media (max-width: 700px) { .stl-print-key__grid { grid-template-columns: repeat(3, 1fr); } }
.stl-print-key__row { margin: 0 0 4px; }
.stl-print-key__cell strong, .stl-print-key__row strong { font-family: var(--stl-font-mono); font-weight: 600; margin-right: 4px; }


/* ============================================================
 * Mode chooser (teacher / student first-run step)
 * Two big tiles in the picker's visual language: icon chip, title,
 * sub, arrow; hover lift + accent ring.
 * ============================================================ */

.stl-modechoice__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin-top: 22px;
}
@media (max-width: 700px) {
  .stl-modechoice__grid { grid-template-columns: 1fr; }
}
.stl-modechoice__tile {
  position: relative;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 10px;
  text-align: left;
  padding: 26px 24px 22px;
  border-radius: 16px;
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  cursor: pointer;
  font-family: var(--stl-font-body);
  color: var(--stl-text);
  transition: transform 160ms var(--ease-out), border-color 160ms ease, background 160ms ease;
}
.stl-modechoice__tile:hover {
  transform: translateY(-2px);
  border-color: var(--stl-accent-line);
  background: var(--stl-surface-2);
}
.stl-modechoice.is-saving .stl-modechoice__tile { opacity: 0.6; pointer-events: none; }
.stl-modechoice__icon {
  display: flex; align-items: center; justify-content: center;
  width: 62px; height: 62px;
  border-radius: 16px;
  background: var(--stl-accent-soft);
  color: var(--stl-accent);
}
#mode-pick-student .stl-modechoice__icon {
  background: rgba(45, 212, 191, 0.12);
  color: #2dd4bf;
}
:root[data-theme="light"] #mode-pick-student .stl-modechoice__icon { color: #0d9488; }
.stl-modechoice__title {
  font-family: var(--stl-font-display);
  font-size: 19px; font-weight: 600;
  margin-top: 4px;
}
.stl-modechoice__sub {
  font-size: 13.5px; line-height: 1.5;
  color: var(--stl-text-muted);
}
.stl-modechoice__arrow {
  position: absolute; top: 24px; right: 22px;
  font-size: 18px; color: var(--stl-text-subtle);
  transition: transform 160ms ease, color 160ms ease;
}
.stl-modechoice__tile:hover .stl-modechoice__arrow {
  transform: translateX(3px);
  color: var(--stl-text);
}

/* ============================================================
 * Teacher dashboard launcher (replaces the tab strip)
 * 2×2 grid of icon cards; each icon chip carries its own hue from
 * the rainbow ramp so the four options read at a glance.
 * ============================================================ */

.stl-teacher__head { position: relative; }
/* Compact top-left back button (Mike 2026-07-25): the button base is
 * full-width flex, which stretched "← Back" across the whole screen on
 * the worksheet builder / teacher sections. Same style, content width,
 * hugging the left — matching the Desmos Advantage back button. */
.stl-teacher__head .stl-teacher__back,
button.stl-teacher__back {
  display: inline-flex;
  width: auto;
  /* The header is a GRID container: grid items stretch to fill their
   * column by default (and inline-flex gets blockified), which is what
   * spanned the button across the screen. justify-self is the grid-
   * axis knob that makes it hug the left at content width. */
  justify-self: start;
  align-self: start;
  width: fit-content;
  margin-bottom: 14px;
}

/* The JS toggles #teacher-back.hidden per tab (hidden on the home
 * launcher, shown inside a section). The author display rule above
 * outranks the UA [hidden] rule, so restore display:none explicitly (same
 * fix as .stl-score__target[hidden]) — otherwise ← Dashboard sticks on
 * the home launcher where it is a no-op. */
.stl-teacher__head .stl-teacher__back[hidden],
button.stl-teacher__back[hidden] { display: none; }

.stl-teacher__launcher {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin-top: 6px;
}
@media (max-width: 760px) {
  .stl-teacher__launcher { grid-template-columns: 1fr; }
}
.stl-launch {
  position: relative;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 9px;
  text-align: left;
  padding: 24px 22px 20px;
  border-radius: 16px;
  border: 1px solid var(--stl-hairline);
  background: var(--stl-surface);
  cursor: pointer;
  font-family: var(--stl-font-body);
  color: var(--stl-text);
  transition: transform 160ms var(--ease-out), border-color 160ms ease, background 160ms ease;
}
.stl-launch:hover {
  transform: translateY(-2px);
  background: var(--stl-surface-2);
  border-color: var(--stl-hairline-hi);
}
.stl-launch__icon {
  display: flex; align-items: center; justify-content: center;
  width: 56px; height: 56px;
  border-radius: 14px;
}
/* Per-card hue (re-toned 2026-08-10, signal palette): tests = brand
 * green, worksheets = teal, students = blue lane, practice = yellow.
 * No violet, no pink, no collision between any two cards.
 *
 * Worksheets was briefly a paper neutral on the reasoning that it
 * prints (Josh 2026-08-10: give it a hue like the rest). Teal is the
 * only slot left that keeps that rule — it sits in the house rainbow
 * between green and sky, and it is far enough from the tests green to
 * read as its own lane at a glance.
 *
 * The tests icon takes --stl-green explicitly rather than --stl-accent.
 * --stl-accent is still violet, so pairing it with a green tile put a
 * violet glyph on a green tile and quietly reintroduced the hue this
 * pass exists to remove. */
.stl-launch__icon--tests      { background: rgba(var(--stl-green-rgb), 0.12);  color: var(--stl-green); }
.stl-launch__icon--worksheets { background: rgba(45, 212, 191, 0.12);  color: #2dd4bf; }
.stl-launch__icon--students   { background: rgba(var(--stl-blue-rgb), 0.12);  color: var(--stl-blue); }
.stl-launch__icon--practice   { background: rgba(var(--stl-yellow-rgb), 0.12);  color: #facc15; }
:root[data-theme="light"] .stl-launch__icon--tests      { color: var(--stl-green); }
:root[data-theme="light"] .stl-launch__icon--worksheets { color: #0d9488; }
:root[data-theme="light"] .stl-launch__icon--students   { color: var(--stl-blue); }
:root[data-theme="light"] .stl-launch__icon--practice   { color: #a16207; }
.stl-launch:hover .stl-launch__icon--tests      { border: 0; box-shadow: inset 0 0 0 1px rgba(var(--stl-green-rgb), 0.4); }
.stl-launch:hover .stl-launch__icon--worksheets { box-shadow: inset 0 0 0 1px rgba(45, 212, 191, 0.4); }
.stl-launch:hover .stl-launch__icon--students   { box-shadow: inset 0 0 0 1px rgba(var(--stl-blue-rgb), 0.4); }
.stl-launch:hover .stl-launch__icon--practice   { box-shadow: inset 0 0 0 1px rgba(var(--stl-yellow-rgb), 0.4); }
.stl-launch__title {
  font-family: var(--stl-font-display);
  font-size: 17px; font-weight: 600;
  margin-top: 4px;
}
.stl-launch__sub {
  font-size: 13px; line-height: 1.5;
  color: var(--stl-text-muted);
}
.stl-launch__arrow {
  position: absolute; top: 22px; right: 20px;
  font-size: 17px; color: var(--stl-text-subtle);
  transition: transform 160ms ease, color 160ms ease;
}
.stl-launch:hover .stl-launch__arrow {
  transform: translateX(3px);
  color: var(--stl-text);
}

/* ======================================================================
 * 2026-07-23 Mike direct batch — spice + rules
 * ==================================================================== */

/* ---- Hide difficulty while TAKING a test (Mike rule 2026-07-22) ------
 * Difficulty stays visible in post-test review/explanations and in Run
 * the Signal (explicit exemption). Presentation-only: JS still writes
 * the pill so reverting is deleting this rule. */
#quiz-difficulty { display: none !important; }

/* ---- Living dot: signal cards' chip reacts to the click --------------
 * (Mike approved 2026-07-22; Notion 3a5f2b8fc5bd81c99b7eeefc344c1cd9.)
 * Green leaps, yellow teeters, red blinks. Runs in parallel with the
 * page-advance transition — never delays lock-in. */
.stl-signal__chip { position: relative; }
.stl-signal__btn--green  .stl-signal__chip.stl-dot-fx { animation: stl-dot-leap 1.05s cubic-bezier(.3,.6,.4,1); }
.stl-signal__btn--yellow .stl-signal__chip.stl-dot-fx { animation: stl-dot-teeter 1.1s ease-in-out; transform-origin: 50% 130%; }
.stl-signal__btn--red    .stl-signal__chip.stl-dot-fx { animation: stl-dot-blink 1.1s steps(1); }
@keyframes stl-dot-leap {
  0%   { transform: translateY(0) scale(1,1); }
  12%  { transform: translateY(2px) scale(1.25,.7); }
  38%  { transform: translateY(-26px) scale(.92,1.1); }
  52%  { transform: translateY(-28px) scale(1,1); }
  74%  { transform: translateY(0) scale(1.35,.6); }
  86%  { transform: translateY(-6px) scale(.95,1.06); }
  100% { transform: translateY(0) scale(1,1); }
}
@keyframes stl-dot-teeter {
  0%   { transform: rotate(0) translateX(0); }
  15%  { transform: translateX(-7px) rotate(-14deg); }
  35%  { transform: translateX(7px) rotate(14deg); }
  52%  { transform: translateX(-5px) rotate(-9deg); }
  68%  { transform: translateX(4px) rotate(6deg); }
  82%  { transform: translateX(-2px) rotate(-3deg); }
  100% { transform: translateX(0) rotate(0); }
}
@keyframes stl-dot-blink {
  0% { opacity: 1; } 12% { opacity: .15; } 28% { opacity: 1; }
  44% { opacity: .15; } 62% { opacity: 1; } 100% { opacity: 1; }
}

/* ---- Length-picker click effects (Mike approved 2026-07-23) ----------
 * 10 Warm-up: bolt zap · 22 One module: dart to the bullseye · 44 Full
 * math: trophy hoist. Effects attach to the FRESH button node after
 * renderLengthPicker() re-renders the row. Coordinates assume the
 * .stl-tier padding (14px 16px) + 18px icon → icon center ≈ (25,23). */
.stl-tier--length { position: relative; overflow: visible; }
.stl-tier--length .stl-tier__icon.stl-lenfx-zap svg { animation: stl-lenfx-jolt .55s ease; }
@keyframes stl-lenfx-jolt {
  0%   { transform: none; filter: none; }
  12%  { transform: translate(-1.5px,1px) scale(1.25); filter: drop-shadow(0 0 6px #f5c518); color: #e8a70c; }
  24%  { transform: translate(2px,-1px) scale(1.18); filter: drop-shadow(0 0 8px #f5c518); color: #e8a70c; }
  36%  { transform: translate(-1.5px,0) scale(1.22); filter: drop-shadow(0 0 6px #f5c518); color: #e8a70c; }
  55%  { transform: scale(1.08); filter: drop-shadow(0 0 3px #f5c518); }
  100% { transform: none; filter: none; }
}
.stl-lenfx-flash { position: absolute; width: 22px; height: 22px; border-radius: 50%;
  pointer-events: none; background: radial-gradient(circle, rgba(245,197,24,.85), rgba(245,197,24,0) 68%);
  animation: stl-lenfx-flash .45s ease forwards; }
@keyframes stl-lenfx-flash { 0% { transform: scale(.3); opacity: 0; } 25% { transform: scale(1.5); opacity: 1; } 100% { transform: scale(2.2); opacity: 0; } }
.stl-lenfx-minibolt { position: absolute; pointer-events: none; font-size: 0; line-height: 0; animation: stl-lenfx-mb .5s ease forwards; }
.stl-lenfx-minibolt svg { width: 9px; height: 9px; color: #e8a70c; }
@keyframes stl-lenfx-mb { 0% { transform: translate(0,0) scale(.6); opacity: 0; } 30% { opacity: 1; } 100% { transform: translate(var(--dx), var(--dy)) scale(1); opacity: 0; } }
.stl-lenfx-dart { position: absolute; pointer-events: none; width: 26px; height: 26px; z-index: 3;
  animation: stl-lenfx-dartfly .38s cubic-bezier(.2,.7,.3,1) forwards; }
.stl-lenfx-dart svg { width: 26px; height: 26px; }
@keyframes stl-lenfx-dartfly {
  0%   { transform: translate(120px,-60px) scale(1.15) rotate(8deg); opacity: 0; }
  25%  { opacity: 1; }
  100% { transform: translate(6.7px,-10.7px) scale(1) rotate(0deg); opacity: 1; }
}
.stl-lenfx-dart.stl-lenfx-stuck { animation: stl-lenfx-wobble .5s ease forwards; transform-origin: 4px 21px; }
@keyframes stl-lenfx-wobble {
  0%   { transform: translate(6.7px,-10.7px) rotate(0); }
  20%  { transform: translate(6.7px,-10.7px) rotate(-7deg); }
  45%  { transform: translate(6.7px,-10.7px) rotate(4deg); }
  70%  { transform: translate(6.7px,-10.7px) rotate(-2deg); }
  100% { transform: translate(6.7px,-10.7px) rotate(0); }
}
.stl-tier--length.stl-lenfx-thunk { animation: stl-lenfx-thunk .28s ease; }
@keyframes stl-lenfx-thunk { 0% { transform: none; } 35% { transform: translate(1.5px,1px); } 70% { transform: translate(-1px,0); } 100% { transform: none; } }
.stl-lenfx-ring { position: absolute; width: 24px; height: 24px; border-radius: 50%;
  border: 2px solid #d94a3a; pointer-events: none; animation: stl-lenfx-ring .5s ease forwards; }
@keyframes stl-lenfx-ring { 0% { transform: scale(.4); opacity: 0; } 30% { opacity: .9; } 100% { transform: scale(1.7); opacity: 0; } }
.stl-tier--length .stl-tier__icon.stl-lenfx-hoist svg { animation: stl-lenfx-hoist 1s cubic-bezier(.3,1.3,.4,1); transform-origin: 50% 90%; }
@keyframes stl-lenfx-hoist {
  0%   { transform: none; }
  25%  { transform: translateY(-7px) rotate(-8deg) scale(1.2); }
  45%  { transform: translateY(-8px) rotate(7deg) scale(1.22); color: #c9930a; }
  65%  { transform: translateY(-7px) rotate(-4deg) scale(1.2); color: #c9930a; }
  85%  { transform: translateY(-2px) rotate(2deg) scale(1.08); }
  100% { transform: none; }
}
.stl-lenfx-gleam { position: absolute; width: 24px; height: 26px; pointer-events: none; overflow: hidden; border-radius: 4px; }
.stl-lenfx-gleam::after { content: ''; position: absolute; top: -4px; left: -14px; width: 9px; height: 36px; transform: rotate(20deg);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.95), transparent); animation: stl-lenfx-gleam .55s ease .25s forwards; }
@keyframes stl-lenfx-gleam { to { left: 30px; } }
.stl-lenfx-conf { position: absolute; width: 5px; height: 5px; pointer-events: none; border-radius: 1px; animation: stl-lenfx-conf .8s ease forwards; }
@keyframes stl-lenfx-conf {
  0% { transform: translate(0,0) rotate(0); opacity: 0; }
  20% { opacity: 1; }
  100% { transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity: 0; }
}

/* ---- Stoplight-drop countdown (Mike approved 2026-07-23) -------------
 * Plays over the quiz/signal screen at start: housing drops on a pole,
 * red 3 → amber 2 → green 1, all-flash GO, snap up, fade. Pure overlay:
 * the screen underneath is already live, so the countdown never delays
 * loading. Skipped under prefers-reduced-motion.
 *
 * Durations here are HALVED to match CD_BEAT = 250 in app.js (Joshua
 * 2026-08-16: "twice as fast"). They are not decorative: the JS lights
 * the first bulb the instant the drop is meant to land, so if these
 * three durations and CD_BEAT ever disagree the housing is still falling
 * while it counts. Change one, change both. */
.stl-cd { position: fixed; inset: 0; z-index: 9999; background: rgba(9,12,24,.96); overflow: hidden; }
.stl-cd.stl-cd-fade { animation: stl-cd-fade .23s ease forwards; }
@keyframes stl-cd-fade { to { opacity: 0; } }
.stl-cd__rig { position: absolute; left: 50%; transform: translate(-50%,-140%); top: 0; text-align: center; }
.stl-cd__rig.stl-cd-drop { animation: stl-cd-drop .35s cubic-bezier(.25,1.25,.45,1) forwards; }
@keyframes stl-cd-drop { 0% { transform: translate(-50%,-140%); } 75% { transform: translate(-50%,9vh); } 88% { transform: translate(-50%,7.6vh); } 100% { transform: translate(-50%,8.4vh); } }
.stl-cd__rig.stl-cd-snapup { animation: stl-cd-snapup .25s cubic-bezier(.6,-.3,.8,.6) forwards; }
@keyframes stl-cd-snapup { 0% { transform: translate(-50%,8.4vh); } 18% { transform: translate(-50%,10.4vh); } 100% { transform: translate(-50%,-150%); } }
.stl-cd__pole { width: 10px; height: 14vh; background: linear-gradient(90deg,#39415e,#232a44 60%); margin: 0 auto; }
.stl-cd__housing { width: 104px; background: #1c2333; border: 2px solid #39415e; border-radius: 18px;
  padding: 14px 0 12px; margin: 0 auto; box-shadow: 0 14px 44px rgba(0,0,0,.55); }
.stl-cd__bulb { width: 58px; height: 58px; border-radius: 50%; background: #2a3350; margin: 9px auto;
  display: flex; align-items: center; justify-content: center; font-size: 30px; font-weight: 800; color: transparent;
  transition: background .1s, box-shadow .1s, color .1s; font-family: var(--stl-font-display); }
.stl-cd__bulb--r.stl-cd-lit { background: #e11d2e; box-shadow: 0 0 34px 8px rgba(225,29,46,.7); color: #3d0409; }
.stl-cd__bulb--y.stl-cd-lit { background: #d9930d; box-shadow: 0 0 34px 8px rgba(217,147,13,.7); color: #3f2a02; }
.stl-cd__bulb--g.stl-cd-lit { background: #22a45d; box-shadow: 0 0 34px 8px rgba(34,164,93,.75); color: #062e18; }
.stl-cd__word { margin-top: 22px; font-family: var(--stl-font-mono); font-size: 13px;
  letter-spacing: .28em; color: #8b93ad; text-transform: uppercase; min-height: 18px; }

/* The passive review sheet's five rules lived here until 2026-08-20.
 * The screen merged into signal's "View all"; the rules went with it
 * rather than lingering as the sixth almost-identical card treatment in
 * this stylesheet. `.stl-signal__card` is the one that survived. */

/* ---- Worksheet → Test share strip (Mike 2026-07-23) ------------------ */
.stl-wk__share { display: flex; gap: 8px; align-items: center; margin: 10px 0 4px; }
/* Harden the hide (Mike 2026-07-26): display:flex above beats the UA
 * [hidden] rule, so the empty share strip (the "long purple box with
 * Copy") was showing before any share happened. It only appears after
 * a teacher shares a test online. */
.stl-wk__share[hidden] { display: none !important; }
.stl-wk__share .stl-input { flex: 1; font-size: 0.85rem; }

/* ---- Learn screens: docked-calculator button (Mike 2026-07-23) ------- */
.stl-learn__calcbtn { margin: 12px 0 4px; }

/* ---- Start-test button: trailing signal light (Mike option B, 2026-07-23) */
.stl-startdots { display: inline-flex; gap: 5px; align-items: center; margin-left: 10px; }
.stl-startdots__d { width: 9px; height: 9px; border-radius: 50%; }
.stl-startdots__d--r { background: var(--stl-red); color: var(--stl-red); }
.stl-startdots__d--y { background: var(--stl-yellow); color: var(--stl-yellow); }
.stl-startdots__d--g { background: var(--stl-green); color: var(--stl-green); }
.stl-startdots--idle .stl-startdots__d { animation: stl-startdots-breathe 2.4s ease-in-out infinite; }
.stl-startdots--idle .stl-startdots__d--r { animation-delay: 0s; }
.stl-startdots--idle .stl-startdots__d--y { animation-delay: .35s; }
.stl-startdots--idle .stl-startdots__d--g { animation-delay: .7s; }
@keyframes stl-startdots-breathe {
  0%, 55%, 100% { box-shadow: 0 0 0 0 transparent; transform: scale(1); }
  22% { box-shadow: 0 0 8px 1.5px currentColor; transform: scale(1.18); }
}
.stl-startdots--go .stl-startdots__d--r { animation: stl-startdots-pop .55s ease; }
.stl-startdots--go .stl-startdots__d--y { animation: stl-startdots-pop .55s ease .12s; }
.stl-startdots--go .stl-startdots__d--g { animation: stl-startdots-pop .55s ease .24s; }
@keyframes stl-startdots-pop {
  0% { transform: scale(1); }
  40% { transform: scale(1.7); box-shadow: 0 0 10px 3px currentColor; }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .stl-startdots--idle .stl-startdots__d, .stl-startdots--go .stl-startdots__d { animation: none; }
}

/* ---- Difficulty tier tiles wear their bucket colors (Mike 2026-07-23):
 * easy green · medium yellow · hard red · insane purple — the same
 * palette as the worksheet grid cells, so difficulty reads identically
 * everywhere. Scoped to [data-tier] so length pills stay neutral. */
/* Tier names inside a hint line carry the same colours as the tiles above
 * them (Joshua 2026-08-17). Kept in this block rather than beside .stl-hint
 * so the four hues stay defined in exactly one place — the comment above is
 * the contract, and a fifth surface using its own greens is how palettes
 * drift. Inherits weight/size from .stl-hint; colour is the only change. */
.stl-hint__tier { font-weight: 600; }
.stl-hint__tier[data-tier="easy"]   { color: var(--stl-green-text); }
.stl-hint__tier[data-tier="medium"] { color: var(--stl-yellow-text); }
.stl-hint__tier[data-tier="hard"]   { color: var(--stl-red-text); }
.stl-hint__tier[data-tier="insane"] { color: var(--stl-accent); }

.stl-tier[data-tier="easy"] .stl-tier__name, .stl-tier[data-composer-tier="easy"] .stl-tier__name { color: var(--stl-green-text); }
.stl-tier[data-tier="medium"] .stl-tier__name, .stl-tier[data-composer-tier="medium"] .stl-tier__name { color: var(--stl-yellow-text); }
.stl-tier[data-tier="hard"] .stl-tier__name, .stl-tier[data-composer-tier="hard"] .stl-tier__name { color: var(--stl-red-text); }
.stl-tier[data-tier="insane"] .stl-tier__name, .stl-tier[data-composer-tier="insane"] .stl-tier__name { color: var(--stl-accent); }
.stl-tier[data-tier="easy"]:not(:disabled), .stl-tier[data-composer-tier="easy"]:not(:disabled) { border-color: rgba(var(--stl-green-rgb), 0.35); }
.stl-tier[data-tier="medium"]:not(:disabled), .stl-tier[data-composer-tier="medium"]:not(:disabled) { border-color: rgba(var(--stl-yellow-rgb), 0.32); }
.stl-tier[data-tier="hard"]:not(:disabled), .stl-tier[data-composer-tier="hard"]:not(:disabled) { border-color: rgba(var(--stl-red-rgb), 0.35); }
.stl-tier[data-tier="insane"]:not(:disabled), .stl-tier[data-composer-tier="insane"]:not(:disabled) { border-color: var(--stl-accent-line); }
.stl-tier[data-tier="easy"].is-active, .stl-tier[data-composer-tier="easy"].is-active { background: rgba(var(--stl-green-rgb), 0.14); border-color: var(--stl-green); }
.stl-tier[data-tier="medium"].is-active, .stl-tier[data-composer-tier="medium"].is-active { background: rgba(var(--stl-yellow-rgb), 0.12); border-color: var(--stl-yellow); }
.stl-tier[data-tier="hard"].is-active, .stl-tier[data-composer-tier="hard"].is-active { background: rgba(var(--stl-red-rgb), 0.14); border-color: var(--stl-red); }
.stl-tier[data-tier="insane"].is-active, .stl-tier[data-composer-tier="insane"].is-active { background: var(--stl-accent-soft); border-color: var(--stl-accent); }
.stl-tier[data-tier].is-active .stl-tier__name, .stl-tier[data-composer-tier].is-active .stl-tier__name { color: inherit; }
.stl-tier[data-tier="easy"].is-active .stl-tier__name, .stl-tier[data-composer-tier="easy"].is-active .stl-tier__name { color: var(--stl-green-text); }
.stl-tier[data-tier="medium"].is-active .stl-tier__name, .stl-tier[data-composer-tier="medium"].is-active .stl-tier__name { color: var(--stl-yellow-text); }
.stl-tier[data-tier="hard"].is-active .stl-tier__name, .stl-tier[data-composer-tier="hard"].is-active .stl-tier__name { color: var(--stl-red-text); }
.stl-tier[data-tier="insane"].is-active .stl-tier__name, .stl-tier[data-composer-tier="insane"].is-active .stl-tier__name { color: var(--stl-accent); }

/* ---- Per-tier hover (Joshua 2026-08-05: "these are missing hover
 * states"). They were not literally missing — the generic
 * `.stl-tier:hover` above still fired — but the per-tier border-color
 * rules right above this one are more specific AND later in the file,
 * so the one part of the hover you actually read, the border, never
 * changed. The generic hover also wanted to paint an accent-purple
 * border, which would have been wrong on a green pill anyway.
 *
 * So each tier gets a hover in its OWN color: the border firms up, a
 * faint wash of the same hue comes in, and the lift shadow is tinted
 * to match. Enough to feel live, well short of looking selected —
 * .is-active still owns the filled treatment. */
.stl-tier[data-tier="easy"]:not(:disabled):not(.is-active):hover,
.stl-tier[data-composer-tier="easy"]:not(:disabled):not(.is-active):hover {
  border-color: rgba(var(--stl-green-rgb), 0.68);
  background: rgba(var(--stl-green-rgb), 0.08);
  box-shadow: 0 6px 16px -10px rgba(var(--stl-green-rgb), 0.55);
}
.stl-tier[data-tier="medium"]:not(:disabled):not(.is-active):hover,
.stl-tier[data-composer-tier="medium"]:not(:disabled):not(.is-active):hover {
  border-color: rgba(var(--stl-yellow-rgb), 0.64);
  background: rgba(var(--stl-yellow-rgb), 0.07);
  box-shadow: 0 6px 16px -10px rgba(var(--stl-yellow-rgb), 0.5);
}
.stl-tier[data-tier="hard"]:not(:disabled):not(.is-active):hover,
.stl-tier[data-composer-tier="hard"]:not(:disabled):not(.is-active):hover {
  border-color: rgba(var(--stl-red-rgb), 0.68);
  background: rgba(var(--stl-red-rgb), 0.08);
  box-shadow: 0 6px 16px -10px rgba(var(--stl-red-rgb), 0.55);
}
.stl-tier[data-tier="insane"]:not(:disabled):not(.is-active):hover,
.stl-tier[data-composer-tier="insane"]:not(:disabled):not(.is-active):hover {
  border-color: var(--stl-accent);
  background: var(--stl-accent-soft);
  box-shadow: 0 6px 16px -10px rgba(var(--stl-tint, var(--stl-rainbow-mid-rgb)), 0.55);
}
/* Already-selected pills still acknowledge the pointer, just by
 * deepening the fill rather than changing the border — the border is
 * carrying "this is the one you picked". */
.stl-tier[data-tier="easy"].is-active:hover,
.stl-tier[data-composer-tier="easy"].is-active:hover { background: rgba(var(--stl-green-rgb), 0.2); }
.stl-tier[data-tier="medium"].is-active:hover,
.stl-tier[data-composer-tier="medium"].is-active:hover { background: rgba(var(--stl-yellow-rgb), 0.18); }
.stl-tier[data-tier="hard"].is-active:hover,
.stl-tier[data-composer-tier="hard"].is-active:hover { background: rgba(var(--stl-red-rgb), 0.2); }
/* Focus ring in the tier's own color too, so keyboard tabbing reads
 * the same as hovering. */
.stl-tier[data-tier="easy"]:focus-visible,
.stl-tier[data-composer-tier="easy"]:focus-visible { outline-color: var(--stl-green); }
.stl-tier[data-tier="medium"]:focus-visible,
.stl-tier[data-composer-tier="medium"]:focus-visible { outline-color: var(--stl-yellow); }
.stl-tier[data-tier="hard"]:focus-visible,
.stl-tier[data-composer-tier="hard"]:focus-visible { outline-color: var(--stl-red); }
/* Touch devices get the color but not the lift, matching the generic
 * (hover: none) carve-out above. */
@media (hover: none) {
  .stl-tier[data-tier]:hover,
  .stl-tier[data-composer-tier]:hover { box-shadow: none; }
}

/* ---- Run the Signal — simple single-pass walkthrough (Mike 2026-07-23) */
.stl-signal__reveal { margin-top: 12px; }
.stl-signal__reveal .stl-review__explain { margin-top: 12px; }
.stl-signal__review-choices { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 6px; }
.stl-signal__gridin-review { margin: 8px 0 0; font-size: 0.95rem; }
.stl-signal__skip { display: block; margin: 10px auto 0; }
.stl-signal__verdict { margin: 12px 0 0; font-weight: 600; }
.stl-signal__verdict--ok { color: var(--stl-green-text); }
.stl-signal__verdict--no { color: var(--stl-red-text); }

/* ---- Run the Signal: Previous/Skip nav row + bucket-colored tier (Mike 2026-07-23) */
/* In-progress resume card (Mike 2026-07-26): sits above the picker /
 * score hero. Accent-tinted border so it reads as "unfinished business"
 * without shouting. */
.stl-resume-host { margin: 0 0 14px; }
.stl-resume-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
  border: 1px solid color-mix(in srgb, var(--stl-accent) 55%, transparent);
  background: color-mix(in srgb, var(--stl-accent) 9%, transparent);
  border-radius: 14px;
}
.stl-resume-card__txt { display: grid; gap: 2px; min-width: 0; }
.stl-resume-card__title { font-weight: 700; }
.stl-resume-card__meta { font-size: 0.9rem; color: var(--stl-text-subtle); }
.stl-resume-card__actions { display: flex; align-items: center; gap: 14px; }
/* Banked green·correct acknowledgment above the first card (Mike
 * 2026-07-26): one muted line with a green-accented count. */
.stl-signal__banked {
  margin: 0 0 12px;
  text-align: center;
  font-size: 0.92rem;
  color: var(--stl-text-subtle);
}
.stl-signal__banked b { color: var(--stl-green); font-weight: 700; }
/* Signal walkthrough nav (2026-08-06). Previously two quiet text links
 * living INSIDE the card, below the two loud action buttons — the most
 * pressed control on the screen was the faintest, and expanding the
 * explanation pushed it out of view. Now a real button pair sitting
 * directly under the card, same secondary treatment as the results
 * quiet links so the whole app reads one way. Two equal columns; on
 * problem 1 there is no Previous, so its partner takes the full row via
 * :only-child rather than a spacer element.
 *
 * 2026-08-20: the row's right-hand slot is now "Back to results" — the
 * screen's exit, moved down out of the flow header (Joshua). It follows
 * the app-wide formula, back on the LEFT of a pair, EXCEPT here: this
 * row's left column is Previous, which is navigation within the walk
 * and has to stay where the hand already expects it. So back takes the
 * right, and the pair reads "step back / leave" rather than the usual
 * "leave / go on". Called out because it is a deliberate break. */
.stl-signal__nav {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 14px;
}
.stl-signal__nav > :only-child { grid-column: 1 / -1; }
/* The exit matches the nav button's height and shape so the row reads as
 * one pair, not a button beside a link. Colour still separates them —
 * .stl-btn--back keeps its own quieter treatment. */
.stl-signal__exit { width: 100%; justify-content: center; }
.stl-signal__navbtn {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; padding: 11px 16px; cursor: pointer;
  background: var(--stl-surface); border: 1px solid var(--stl-hairline-hi);
  border-radius: var(--r-pill);
  color: var(--stl-text); font: inherit; font-size: .95rem; font-weight: 550;
  white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, transform .05s ease;
}
.stl-signal__navbtn:hover {
  background: var(--stl-surface-2);
  border-color: var(--stl-text-faint);
}
.stl-signal__navbtn:active { transform: translateY(1px); }
/* The arrows are decoration, so they sit a shade back from the label and
 * do the moving on hover. */
.stl-signal__nav-arrow { color: var(--stl-text-subtle); transition: transform .15s ease; }
.stl-signal__nextq:hover .stl-signal__nav-arrow { transform: translateX(2px); }
.stl-signal__prev:hover .stl-signal__nav-arrow { transform: translateX(-2px); }
/* tier label wears its bucket color, same palette as the difficulty tiles */
.stl-signal__tier[data-tier="easy"]   { color: var(--stl-green-text); }
.stl-signal__tier[data-tier="medium"] { color: var(--stl-yellow-text); }
.stl-signal__tier[data-tier="hard"]   { color: var(--stl-red-text); }
.stl-signal__tier[data-tier="insane"] { color: var(--stl-accent); }

/* ============== mobile landing ("Value-Forward", Josh 2026-07-26) ====
 * The <=640px variant of the mode screen. Desktop keeps the two-tile
 * chooser; at phone widths the tiles hide and this block renders:
 * H1 -> three why-rows (marks shared with the picker whylist) ->
 * student/teacher segmented control -> one primary CTA -> legal.
 * While this landing is the visible screen on a phone we also trim the
 * header to brand + auth: the nav tabs, theme toggle, and New-test CTA
 * are noise for a first-time visitor (the :has pattern matches the
 * admin/teacher width rules above). All colors ride theme tokens, so
 * light mode works for free; the only fixed colors are the three
 * signal dots inside .stl-why__mark--signal. */
.stl-mland { display: none; }
@media (max-width: 640px) {
  [data-screen="mode"] .stl-modechoice { display: none; }
  body:has([data-screen="mode"]:not([hidden])) .stl-header__nav,
  body:has([data-screen="mode"]:not([hidden])) #btn-toggle-theme,
  body:has([data-screen="mode"]:not([hidden])) #btn-new-test { display: none; }

  .stl-mland {
    display: flex;
    flex-direction: column;
    padding: 6px 2px 10px;
  }
  .stl-mland__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
    padding-top: env(safe-area-inset-top);
  }
  .stl-mland__brand { width: 128px; height: auto; color: var(--stl-text); display: block; }
  .stl-mland__signin {
    font-size: 12.5px;
    font-weight: 500;
    color: var(--stl-text-muted);
    background: transparent;
    border: 1px solid var(--stl-hairline-hi);
    border-radius: var(--r-pill);
    padding: 8px 15px;
    font-family: var(--stl-font-body);
    cursor: pointer;
  }
  .stl-mland__h1 {
    font-family: var(--stl-font-display);
    font-weight: 700;
    font-size: 24px;
    line-height: 1.1;
    letter-spacing: -0.022em;
    color: var(--stl-text);
  }
  .stl-mland__whylist {
    list-style: none;
    margin: 14px 0 0;
    padding: 0;
  }
  .stl-mland__whylist li {
    display: flex;
    gap: 13px;
    align-items: flex-start;
    padding: 12px 0;
    border-top: 1px solid var(--stl-hairline);
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--stl-text-muted);
  }
  .stl-mland__whylist li:first-child { border-top: 0; }
  .stl-mland__whylist strong { color: var(--stl-text); font-weight: 600; }
  .stl-mland__seg {
    display: flex;
    background: var(--stl-surface);
    border: 1px solid var(--stl-hairline);
    border-radius: 15px;
    padding: 4px;
    margin-top: 12px;
  }
  .stl-mland__segbtn {
    flex: 1;
    height: 44px; /* full-size touch target */
    border: 0;
    border-radius: 11px;
    background: transparent;
    color: var(--stl-text-subtle);
    font-family: var(--stl-font-display);
    font-weight: 600;
    font-size: 13.5px;
    cursor: pointer;
  }
  .stl-mland__segbtn.is-on,
  .stl-mland__segbtn[aria-checked="true"] {
    background: var(--stl-accent-soft);
    color: var(--stl-accent);
    border-color: var(--stl-accent-line);
  }
  .stl-mland__cap {
    font-size: 11.5px;
    line-height: 1.5;
    color: var(--stl-text-subtle);
    text-align: center;
    margin: 10px 4px 0;
    /* fixed height for the taller of the two captions so the CTA never
     * shifts when the segment toggles */
    min-height: 36px;
  }
  .stl-mland__cta {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    height: 54px;
    border: 0;
    border-radius: 17px;
    margin-top: 8px;
    font-family: var(--stl-font-display);
    font-weight: 600;
    font-size: 15.5px;
    color: #0b0c14;
    background: linear-gradient(115deg, var(--stl-accent) 10%, var(--stl-accent-2) 90%);
    box-shadow: 0 10px 26px -10px rgba(var(--stl-rainbow-mid-rgb), 0.45);
    cursor: pointer;
  }
  .stl-mland__cta:active { transform: translateY(1px); }
  /* Light theme darkens the accent steps, so the near-black label loses
   * contrast on the gradient — flip the CTA text to white there. */
  :root[data-theme="light"] .stl-mland__cta { color: #fff; }
  [data-screen="mode"].is-saving .stl-mland__cta { opacity: 0.6; pointer-events: none; }
  .stl-mland__legal {
    font-size: 10px;
    line-height: 1.55;
    color: var(--stl-text-faint);
    text-align: center;
    margin: 18px 6px 0;
  }
}

/* ============== picker: Lean Grid + the Living Home (Josh 2026-07-26) =
 * Mobile-only redesign of the test picker. Two states on one screen:
 * no attempts -> the compact Lean Grid (2-across tinted tiles, Soon
 * chips on non-live tests, whylist hidden — the landing owns that copy
 * now); any attempts -> .is-phome on the section swaps in the Living
 * Home (continue card + past tests + Desmos row). The shared brand row
 * (.stl-mbrand) mirrors the landing bar geometry exactly. Desktop is
 * completely untouched: everything here is display:none >640px. */
.stl-mbrand, .stl-phome, .stl-picker__soon { display: none; }

@media (max-width: 640px) {
  /* brand row — same geometry as the landing bar */
  [data-screen="picker"] .stl-mbrand {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: calc(15px + env(safe-area-inset-top)) 18px 8px;
  }
  .stl-mbrand__wordmark { width: 122px; height: auto; color: var(--stl-text); display: block; }
  .stl-mbrand__signin {
    font-size: 12.5px; font-weight: 500; color: var(--stl-text-muted);
    background: transparent; border: 1px solid var(--stl-hairline-hi);
    border-radius: var(--r-pill); padding: 8px 15px; font-family: var(--stl-font-body); cursor: pointer;
  }
  .stl-mbrand__menu {
    width: 38px; height: 38px; border-radius: 12px;
    border: 1px solid var(--stl-hairline-hi); background: transparent;
    display: grid; place-items: center; color: var(--stl-text-muted); cursor: pointer;
  }
  /* the old mobile bar is replaced by the brand row */
  [data-screen="picker"] .stl-mobile-bar { display: none; }
  /* The landing owns the pitch now. This used to target
   * `.stl-picker__whylist`, which was deleted on 2026-08-05 and replaced
   * by `.stl-whyband` above the card, so the selector was pointing at
   * nothing and the band was leaking onto phones. The re-open link goes
   * with it: with no band to re-open, a "Why this works" control here
   * would be a button that does nothing visible. */
  [data-screen="picker"] .stl-whyband,
  [data-screen="picker"] .stl-whyband__reopen { display: none; }
  [data-screen="picker"] .stl-picker .stl-h1 { font-size: 20px; }

  /* Lean Grid: two-across compact tinted tiles */
  [data-screen="picker"] .stl-picker__grid {
    grid-template-columns: 1fr 1fr;
    gap: 9px;
  }
  [data-screen="picker"] .stl-picker__tile { padding: 12px 11px 10px; border-radius: 15px; }
  [data-screen="picker"] .stl-picker__mark { transform: scale(.8); transform-origin: left top; margin-bottom: 0; }
  [data-screen="picker"] .stl-picker__name { font-size: 14px; }
  [data-screen="picker"] .stl-picker__tagline { font-size: 9px; }
  [data-screen="picker"] .stl-picker__tile.is-soon { opacity: .55; }
  [data-screen="picker"] .stl-picker__soon {
    display: block; position: absolute; top: 9px; right: 9px;
    font-family: var(--stl-font-display); font-weight: 600; font-size: 8px;
    letter-spacing: .06em; text-transform: uppercase; color: var(--stl-text-subtle);
    border: 1px solid var(--stl-hairline-hi); border-radius: var(--r-pill); padding: 2px 6px;
  }
  /* feature cards compact into rows */
  [data-screen="picker"] .stl-featurecard { flex-direction: row; align-items: center; gap: 11px; padding: 12px; }
  [data-screen="picker"] .stl-featurecard__icon { width: 32px; height: 32px; margin: 0; flex: 0 0 auto; }
  [data-screen="picker"] .stl-featurecard__icon svg { width: 16px; height: 16px; }
  [data-screen="picker"] .stl-featurecard__title { font-size: 12.5px; }
  [data-screen="picker"] .stl-featurecard__sub { font-size: 10px; margin: 2px 0 0; }
  [data-screen="picker"] .stl-featurecard__cta { display: none; }

  /* ---- the Living Home ---- */
  [data-screen="picker"].is-phome .stl-card.stl-picker { display: none; }
  [data-screen="picker"].is-phome .stl-phome {
    display: flex; flex-direction: column; padding: 2px 18px 20px;
  }
  .stl-phome__h1 {
    font-family: var(--stl-font-display); font-weight: 700;
    font-size: 21px; letter-spacing: -.022em; line-height: 1.1; color: var(--stl-text);
  }
  .stl-phome__cont {
    border-radius: 19px; border: 1px solid var(--stl-hairline-hi);
    background: var(--stl-surface-2); padding: 14px; margin-top: 12px;
  }
  .stl-phome__cap {
    font-family: var(--stl-font-mono); font-size: 8.5px; letter-spacing: .14em;
    text-transform: uppercase; color: var(--stl-text-subtle);
  }
  .stl-phome__t { font-family: var(--stl-font-display); font-weight: 700; font-size: 16px; margin-top: 5px; color: var(--stl-text); }
  .stl-phome__bar { display: flex; height: 6px; border-radius: 6px; overflow: hidden; margin-top: 10px; background: var(--stl-surface-3); }
  .stl-phome__bar i { display: block; height: 100%; }
  .stl-phome__bar i:nth-child(1) { background: var(--stl-green); }
  .stl-phome__bar i:nth-child(2) { background: var(--stl-yellow); }
  .stl-phome__bar i:nth-child(3) { background: var(--stl-red); }
  .stl-phome__sigcap { display: flex; justify-content: space-between; font-size: 9px; color: var(--stl-text-subtle); margin-top: 5px; }
  .stl-phome__btns { display: flex; gap: 8px; margin-top: 11px; }
  .stl-phome__b1 {
    flex: 1.4; height: 44px; border-radius: 12px; border: 0; cursor: pointer;
    font-family: var(--stl-font-display); font-weight: 600; font-size: 12.5px; color: #0b0c14;
    background: linear-gradient(115deg, var(--stl-accent), var(--stl-accent-2));
  }
  :root[data-theme="light"] .stl-phome__b1 { color: #fff; }
  .stl-phome__b2 {
    flex: 1; height: 44px; border-radius: 12px; cursor: pointer;
    border: 1px solid var(--stl-hairline-hi); background: transparent;
    color: var(--stl-text); font-family: var(--stl-font-display); font-weight: 600; font-size: 12.5px;
  }
  .stl-phome__caprow {
    font-family: var(--stl-font-mono); font-size: 8.5px; letter-spacing: .13em;
    text-transform: uppercase; color: var(--stl-text-subtle); margin-top: 14px;
  }
  .stl-phome__hrow {
    display: flex; align-items: center; gap: 11px; width: 100%;
    padding: 11px 2px; border: 0; border-bottom: 1px solid var(--stl-hairline);
    background: transparent; text-align: left; cursor: pointer; color: inherit;
  }
  .stl-phome__hmeta { flex: 1; min-width: 0; }
  .stl-phome__ht { display: block; font-family: var(--stl-font-display); font-weight: 600; font-size: 12px; color: var(--stl-text); }
  .stl-phome__hd { display: block; font-size: 9.5px; color: var(--stl-text-subtle); margin-top: 2px; }
  .stl-phome__hrow .stl-phome__bar { width: 64px; flex: 0 0 auto; margin-top: 0; }
  .stl-phome__harw { color: var(--stl-text-faint); font-size: 12px; }
  .stl-phome__seeall {
    display: flex; align-items: center; gap: 5px; border: 0; background: transparent;
    font-size: 11px; color: var(--stl-accent); font-weight: 500; padding: 10px 2px 0;
    cursor: pointer; font-family: var(--stl-font-body);
  }
  .stl-phome__lrow {
    display: flex; gap: 11px; align-items: center; width: 100%;
    padding: 12px; border-radius: 14px; border: 1px solid var(--stl-hairline);
    background: var(--stl-surface); margin-top: 12px; text-align: left; cursor: pointer; color: inherit;
  }
  .stl-phome__lic {
    flex: 0 0 auto; width: 32px; height: 32px; border-radius: 10px;
    display: grid; place-items: center;
    background: rgba(var(--stl-green-rgb), .12); color: var(--stl-green);
  }
  .stl-phome__lmeta { flex: 1; min-width: 0; }
  .stl-phome__lt { display: block; font-family: var(--stl-font-display); font-weight: 600; font-size: 12.5px; color: var(--stl-text); }
  .stl-phome__ld { display: block; font-size: 10px; color: var(--stl-text-subtle); margin-top: 2px; line-height: 1.35; }
}

/* ---- menu bottom sheet (any width; only reachable from mobile UI) ---- */
.stl-msheet { position: fixed; inset: 0; z-index: 120; }
.stl-msheet[hidden] { display: none; }
.stl-msheet__scrim { position: absolute; inset: 0; background: rgba(5, 5, 9, .62); }
.stl-msheet__panel {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: var(--stl-bg); border-radius: 22px 22px 0 0;
  border-top: 1px solid var(--stl-hairline-hi);
  padding: 10px 16px calc(20px + env(safe-area-inset-bottom));
  max-width: 640px; margin: 0 auto;
}
.stl-msheet__grab { width: 38px; height: 4px; border-radius: 4px; background: var(--stl-hairline-hi); margin: 2px auto 10px; }
.stl-msheet__who { display: flex; align-items: center; gap: 10px; padding: 10px 4px 12px; border-bottom: 1px solid var(--stl-hairline); }
.stl-msheet__avatar {
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--stl-accent-soft); color: var(--stl-accent);
  display: grid; place-items: center; font-family: var(--stl-font-display); font-weight: 700; font-size: 13px;
}
.stl-msheet__email { display: block; font-size: 12px; color: var(--stl-text-muted); }
.stl-msheet__role { display: block; font-size: 9.5px; color: var(--stl-text-subtle); }
.stl-msheet__row {
  display: flex; align-items: center; gap: 12px; width: 100%; min-height: 46px;
  padding: 0 4px; border: 0; border-bottom: 1px solid var(--stl-hairline);
  background: transparent; cursor: pointer; text-align: left;
  font-family: var(--stl-font-display); font-weight: 600; font-size: 13.5px; color: var(--stl-text);
}
.stl-msheet__row svg { color: var(--stl-text-muted); flex: 0 0 auto; }
.stl-msheet__row--quiet { color: var(--stl-text-muted); border-bottom: 0; }
.stl-msheet__aux { margin-left: auto; font-size: 11px; color: var(--stl-text-subtle); font-family: var(--stl-font-body); font-weight: 400; }
/* Author display rules above outrank the UA [hidden] rule — restore it
 * explicitly (same fix as .stl-score__target[hidden] / the teacher back
 * button). Without these, guests see Sign out + Teacher dashboard. */
.stl-msheet__row[hidden],
.stl-mbrand__signin[hidden],
.stl-mbrand__menu[hidden] { display: none; }

/* ============== Smart Start (build-a-test, Josh 2026-07-26) ==========
 * <=640px re-layout of the score screen: lead with the summary card
 * (built from saved settings), collapse difficulty/length behind
 * accordion headers that toggle .is-open on the existing sections,
 * stick the Start CTA in thumb reach, prune the footer to the baseline
 * link. Desktop form untouched — everything below gates to <=640px.
 * All [hidden]-toggled or display-styled elements carry explicit
 * [hidden]{display:none} companions per the recurring-bug rule. */
.stl-smart__h1, .stl-smart__sum, .stl-smart__lbl, .stl-smart__accrow { display: none; }

@media (max-width: 640px) {
  /* desktop headline + drifted sub give way to the smart layout */
  [data-screen="score"] .stl-score > .stl-h1,
  [data-screen="score"] #score-sub { display: none; }

  [data-screen="score"] .stl-smart__h1 {
    display: block;
    font-family: var(--stl-font-display);
    font-weight: 700;
    font-size: 24px;
    line-height: 1.1;
    letter-spacing: -0.022em;
    color: var(--stl-text);
  }
  [data-screen="score"] .stl-smart__sum {
    display: block;
    border-radius: 19px;
    border: 1px solid var(--stl-hairline-hi);
    background: var(--stl-surface-2);
    padding: 15px;
    margin-top: 13px;
  }
  .stl-smart__big { font-family: var(--stl-font-display); font-weight: 700; font-size: 19px; color: var(--stl-text); }
  .stl-smart__mini { font-size: 11px; color: var(--stl-text-muted); margin-top: 4px; line-height: 1.5; }
  .stl-smart__chips { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
  .stl-smart__chip {
    display: flex; align-items: center; gap: 5px;
    font-family: var(--stl-font-display); font-weight: 600; font-size: 10.5px;
    color: var(--stl-text-muted); border: 1px solid var(--stl-hairline);
    border-radius: var(--r-pill); padding: 5px 10px;
  }
  .stl-smart__sw { display: flex; gap: 3px; }
  .stl-smart__sw i { width: 5px; height: 5px; border-radius: 50%; display: block; }

  [data-screen="score"] .stl-smart__lbl {
    display: block;
    font-family: var(--stl-font-mono); font-size: 8.5px; letter-spacing: .14em;
    text-transform: uppercase; color: var(--stl-text-subtle); margin: 16px 0 7px;
  }
  [data-screen="score"] .stl-smart__accrow {
    display: flex; align-items: center; gap: 11px; width: 100%; min-height: 46px;
    padding: 0 14px; border: 1px solid var(--stl-hairline); border-radius: 15px;
    background: var(--stl-surface); margin-top: 8px; cursor: pointer; text-align: left;
  }
  .stl-smart__acct { font-family: var(--stl-font-display); font-weight: 600; font-size: 12.5px; color: var(--stl-text); }
  .stl-smart__accval { margin-left: auto; font-size: 11px; color: var(--stl-text-subtle); text-align: right; }
  .stl-smart__accch { color: var(--stl-text-faint); font-size: 12px; transition: transform 160ms ease; }
  .stl-smart__accrow.is-open .stl-smart__accch { transform: rotate(90deg); }

  /* the existing sections become the accordion bodies */
  [data-screen="score"] .stl-score__difficulty:not(.is-open),
  [data-screen="score"] .stl-score__length:not(.is-open) { display: none; }
  [data-screen="score"] .stl-score__difficulty.is-open,
  [data-screen="score"] .stl-score__length.is-open {
    border: 1px solid var(--stl-hairline);
    border-radius: 15px;
    background: var(--stl-surface);
    padding: 12px 12px 13px;
    margin-top: 6px;
  }
  [data-screen="score"] .stl-score__difficulty .stl-score__section-head,
  [data-screen="score"] .stl-score__length .stl-score__section-head { display: none; }

  /* compact 4-across chips: color carries the state, no glyphs */
  [data-screen="score"] #score-tiers,
  [data-screen="score"] #score-length {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 7px;
  }
  [data-screen="score"] .stl-tier { padding: 9px 4px; min-height: 44px; }
  /* Score ranges (≤500, 510–600…) are dropped here to keep the four
   * buckets to compact pills. Tailored is exempt: "Auto adjusted" is not
   * a score range, it is the only thing on the tile that explains what
   * the control does, and the tile has a full row to itself anyway. */
  [data-screen="score"] .stl-score__difficulty .stl-tier__range { display: none; }
  [data-screen="score"] .stl-score__difficulty .stl-tier--tailored .stl-tier__range { display: inline; }
  [data-screen="score"] .stl-tier--length .stl-tier__icon { display: none; }
  [data-screen="score"] .stl-tier__name { font-size: 12.5px; }
  [data-screen="score"] .stl-tier--length .stl-tier__range { font-size: 8.5px; }

  /* Start stays in thumb reach; footer prunes to the baseline link */
  [data-screen="score"] #score-start-test {
    position: sticky;
    bottom: calc(14px + env(safe-area-inset-bottom));
    z-index: 5;
    margin-top: 16px;
  }
  /* Back is hidden here, not because it does not matter, but because the
   * mobile bar's left slot already carries it on this screen — the same
   * one-back-control rule the quiz follows. The stacked desktop layout
   * would otherwise put a second one directly under the sticky Start.
   * (#score-learn-entry was also listed here until 2026-08-16; that
   * element no longer exists.) */
  [data-screen="score"] #score-back-to-picker { display: none; }
}

/* ====================================================================
 * Mobile results redesign (Mike 2026-07-26): the phone results page
 * was a mile-long stack — six full-height ledger rows plus five
 * full-width utility buttons pushed "Run the Signal" (the one action
 * that matters) three screens below the fold. At ≤640px the desktop
 * two-column grid is flattened (display: contents) so every block is
 * re-orderable, and the page tells the story in priority order:
 *   pills → hero score → RUN THE SIGNAL → domain results →
 *   compact 2×3 ledger grid → one quiet row of utility links.
 * Desktop is untouched.
 * ================================================================== */
@media (max-width: 640px) {
  [data-screen="results"] .stl-results { display: flex; flex-direction: column; gap: 14px; }
  [data-screen="results"] .stl-results > * { order: 50; margin: 0; }
  /* Three levels of display:contents, not two. The actions now live
   * INSIDE the domains section (desktop right column), so that section
   * has to flatten as well or its children would order themselves
   * against each other inside a nested box instead of against the page.
   * Every flattened block therefore needs an explicit order — a missing
   * one lands at 0 and jumps above the banners. */
  .stl-results__top, .stl-results__left-col, .stl-results__domains-section { display: contents; }
  [data-screen="results"] .stl-results #shared-banner { order: 1; }
  [data-screen="results"] .stl-results #assessment-banner { order: 2; }
  [data-screen="results"] .stl-results .stl-results__head-row { order: 3; }
  [data-screen="results"] .stl-results .stl-results__overview { order: 4; }
  /* The footer pair (back + Run the Signal) moved to the BOTTOM on
   * mobile too (Josh 2026-08-07). It used to sit at order 5, right
   * under the hero and above the domain bars. Now that back rides with
   * it they travel as one unit, and splitting them to honour the old
   * position would put back near the top of the screen — the opposite
   * of "drop it down below". Orders 5/6 are intentionally vacant. */
  /* Same order value on the header and the bars — equal order keeps DOM
   * order, so the "Your Results" eyebrow still sits on top of them. */
  [data-screen="results"] .stl-results .stl-results__domains-head { order: 7; }
  [data-screen="results"] .stl-results .stl-results__domains { order: 7; }
  [data-screen="results"] .stl-results .stl-results__quietlinks { order: 8; }
  /* The two mode buttons, then their done-note, then Back last. Without
   * an explicit order this block inherited `order: 50` from the `> *`
   * rule above and rendered BELOW Back — so the phone showed "Back" and
   * then, underneath it, the two things you actually came here to do. */
  [data-screen="results"] .stl-results .stl-results__modes { order: 9; }
  [data-screen="results"] .stl-results #retest-done-note { order: 10; }
  [data-screen="results"] .stl-results .stl-results__footer-actions { order: 11; }

  /* Pills: allow tight wrap, no overflow. */
  .stl-results__pacing { flex-wrap: wrap; row-gap: 6px; }

  /* Ledger: 6 tall rows → compact tappable 2×3 grid. The "sure +
   * correct" hints are desktop flavor — the color dot + label carry
   * the meaning on a phone. */
  .stl-results__ledger { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .stl-results__ledger .stl-results__row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 8px; padding: 10px 12px; min-width: 0;
  }
  .stl-results__ledger .stl-results__row-hint { display: none; }
  .stl-results__ledger .stl-results__row-title { font-size: .88rem; }
  .stl-results__ledger .stl-results__row-stats { gap: 6px; }
  .stl-results__ledger .stl-results__row-pct { font-size: .72rem; }

  /* Run the Signal: fills its half of the footer pair. */
  .stl-results__footer-actions .stl-btn--signal { width: 100%; justify-content: center; }

  /* Utility links: five full-width buttons → one quiet wrapped row of
   * plain text links (Review keeps its count). */
  .stl-results__quietlinks {
    display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center;
    gap: 4px 18px; padding-top: 2px;
  }
  .stl-results__quietlinks .stl-results__qlink {
    width: auto; flex: 0 0 auto;
    padding: 8px 2px; border: 0; background: none; box-shadow: none;
    font-size: .92rem; color: var(--stl-text-subtle);
    text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--stl-hairline-hi);
  }
  .stl-results__quietlinks .stl-results__qlink:active { color: var(--stl-text); }
}

/* ===================================================================
 * A11Y FLOOR — FIXLIST P2, 2026-08-18
 * ===================================================================
 * One consolidated block, deliberately: these ~35 components each lacked
 * a :focus-visible ring (most because they re-implement .stl-btn from
 * scratch and never inherited its ring). Keeping the additions in one
 * place makes the coverage auditable and lets tests/a11y-floor.test.js
 * assert against a single region. When a component is later rebased onto
 * .stl-btn/.stl-seg (FIXLIST P3), delete its line here.
 *
 * The ring is the base ring from .stl-btn:focus-visible. Components that
 * already had their own ring are NOT repeated here.
 * =================================================================== */
.stl-signal__sig:focus-visible,
.stl-signal__qlink:focus-visible,
.stl-signal__navbtn:focus-visible,
.stl-results__qlink:focus-visible,
.stl-results__domain-head:focus-visible,
.stl-desmos-panel__close:focus-visible,
.stl-refsheet__close:focus-visible,
.stl-desmos-section__summary:focus-visible,
.stl-faq summary:focus-visible,
.stl-featurecard:focus-visible,
.stl-modechoice__tile:focus-visible,
.stl-launch:focus-visible,
.stl-mytests__row:focus-visible,
.stl-roster__row:focus-visible,
.stl-chip:focus-visible,
.stl-topicdd__toggle:focus-visible,
.stl-topicdd__opt:focus-visible,
.stl-msdd__trigger:focus-visible,
.stl-msdd__row:focus-visible,
.stl-msdd__clear:focus-visible,
.stl-wk__cell:focus-visible,
.stl-wk__bulk:focus-visible,
.stl-admin__pill-toggle:focus-visible,
.stl-admin__th--sortable:focus-visible,
tr.stl-users__row:focus-visible,
.stl-add-modal__back:focus-visible,
.stl-dropzone:focus-visible,
.stl-add-image__tab:focus-visible,
.stl-preview-banner__close:focus-visible,
.stl-review-modal__participant:focus-visible,
.stl-mobile-bar__btn:focus-visible,
.stl-msheet__row:focus-visible,
.stl-mland__signin:focus-visible,
.stl-mland__segbtn:focus-visible,
.stl-mland__cta:focus-visible,
.stl-mbrand__signin:focus-visible,
.stl-mbrand__menu:focus-visible,
.stl-phome__b1:focus-visible,
.stl-phome__b2:focus-visible,
.stl-phome__hrow:focus-visible,
.stl-phome__seeall:focus-visible,
.stl-phome__lrow:focus-visible,
.stl-smart__accrow:focus-visible,
.stl-learn__card:focus-visible,
.stl-learn__choice:focus-visible,
.stl-learn__cheat-row:focus-visible,
.stl-teachx__arrow:focus-visible,
.stl-teachx__dot:focus-visible,
.stl-score__back-foot:focus-visible,
.stl-score__subjects-toggle:focus-visible,
.stl-score__custom-toggle:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: 2px;
}
/* Inset variants — controls whose geometry clips an outset ring. */
.stl-how__scope button:focus-visible {
  outline: 2px solid var(--stl-accent);
  outline-offset: -2px;
}
/* The review-card note textarea removed the outline and replaced it with a
 * near-invisible border tint; give it the .stl-input focus treatment. */
.stl-review-card__note-input:focus-visible {
  outline: none;
  border-color: var(--stl-accent);
  box-shadow: 0 0 0 3px var(--stl-accent-soft);
}

/* Hover-lift resets for touch — a tap otherwise leaves the card stuck in
 * its hovered pose until something else repaints it. */
@media (hover: none) {
  .stl-launch:hover,
  .stl-modechoice__tile:hover,
  .stl-featurecard:hover,
  .stl-step:hover,
  .stl-how__sec:hover,
  button.stl-admin__kpi:hover,
  .stl-admin__icon-btn:hover,
  .stl-wk__bulk:hover,
  #wk-builder .stl-chip:hover {
    transform: none;
  }
  .stl-mytests__row:hover .stl-mytests__chev,
  .stl-launch:hover .stl-launch__arrow {
    transform: none;
  }
  .stl-admin__row:hover {
    box-shadow: none;
  }
}
