/* LORE admin console (2026-09-08).
 *
 * One stylesheet, no framework — same posture as portal-admin/: this has to
 * be readable by whoever maintains it next, and a build step for one page is
 * a liability rather than a convenience.
 *
 * Colour: every colour is a token; the chart colours are the CVD-validated
 * set from the admin dashboard and must not be substituted without
 * re-validating (see CLAUDE.md). The palette is Shawn's (2026-10-16): his
 * dark one exactly in LORE Blue (the default) and Dark, a light counterpart
 * of it in Light, and Easy reading as it was. The faculty course page reads
 * the same tokens (workspace-theme.css), so one theme dresses every tab.
 */

:root {
  /* ===== DESIGN TOKENS (2026-09-23, design foundation) =====================
     Every colour, size, shadow and duration on this console is one of these.
     B/C/D's cards.css, charts.css and media.css read them; they never define
     their own palette. Names without a number (--surface, --shadow, --radius)
     are the originals and stay as aliases so older rules keep working. */

  /* Type. SHAWN'S FACE (2026-10-16): both of his design studies declare
     `--sans: "Atkinson Hyperlegible", "Segoe UI", system-ui, sans-serif`, and LORE
     serves Atkinson Hyperlegible itself (console/fonts/, OFL: a university's
     pages must not call a font CDN), so that stack is the console's, in every
     theme but Scholastic (its serif). Atkinson comes in two weights, 400 and 700,
     so the weights below are those two: a medium is the regular, a semibold the
     bold (a 520 or a 620 would be drawn as the 700 anyway). Inter is still
     declared, behind Atkinson in Easy reading. Tabular figures via .tnum. */
  --font-sans: "Atkinson Hyperlegible", "Segoe UI", system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --fs-display: 2rem;       /* the one number or sentence a page is about */
  --fs-h1: 1.5rem;
  --fs-h2: 1.1875rem;
  --fs-h3: 1rem;
  --fs-body: .9375rem;      /* 15px */
  --fs-small: .8125rem;
  --fs-caption: .75rem;
  --lh-tight: 1.2;
  --lh: 1.55;
  --fw-regular: 400;
  --fw-medium: 400;
  --fw-semibold: 700;
  --fw-bold: 700;
  --tracking-tight: -.018em;
  --tracking-caps: .06em;

  /* Surfaces: page, then 1 (cards), 2 (a fill inside a card), 3 (a stronger
     fill: hover on a fill, a track).
     LIGHT IS THE LIGHT COUNTERPART OF SHAWN'S PALETTE (2026-10-16; the dark one is
     LORE Blue's, below): the same cool blue-grey family, his ink (#0e1928, his
     panel) on white, his four hues (blue, green, amber, purple) darkened until
     they read at 4.5:1 on white and on --surface-2. --border-strong, the edge of
     a button or a field, holds 3:1 on white and on the page. Measured: text
     17.7:1 on white, 16.3 on the page; --text-dim 6.7 on white, 6.0 on
     --surface-2; accent 6.1, ok 6.1, warn 5.9, danger 7.4, info 7.2 on white;
     --border-strong 3.55 on white, 3.28 on the page. */
  --bg: #f3f6fa;
  --surface-1: #ffffff;
  --surface-2: #eef3f8;
  --surface-3: #e2e9f1;
  --border: #d5dee8;
  --border-strong: #768aa0;
  --text: #0e1928;
  --text-dim: #4b5e73;       /* 6.7:1 on white, 6.0:1 on --surface-2 */
  --accent: #0b6896;         /* Shawn's #78d1ff, darkened: 6.1:1 on white */
  --accent-soft: #e3f1fa;
  --on-accent: #ffffff;      /* words on an --accent fill: 6.1:1 */

  /* Semantic. Each colour reads at 4.5:1 on its own -soft fill and on white. */
  --ok: #17704f;       --ok-soft: #e2f3ec;
  --warn: #8a5a06;     --warn-soft: #fbf0dc;
  --danger: #a3242a;   --danger-soft: #fbe6e7;
  --info: #5b46ad;     --info-soft: #efebfb;   /* Shawn's purple, darkened */

  /* Charts: the CVD-validated set (CLAUDE.md). Do not substitute. */
  --chart-1: #4FB3D9;
  --chart-2: #8B6CFF;
  --chart-3: #E8A33D;

  /* Elevation (2026-10-11): surfaces are FLAT and separated by a hairline
     border, not by a shadow. -sm is nothing; -md is the faintest lift, for a
     bar pinned over scrolling text; -lg is kept for what really floats over
     the page (a dialog, a menu, a toast, a row being dragged). -sm is a
     zero shadow rather than `none` because it is used inside lists
     (`box-shadow: 0 0 0 2px var(--focus), var(--shadow-md)`), where `none`
     would void the whole declaration and take the focus ring with it. */
  --shadow-sm: 0 0 0 0 transparent;
  --shadow-md: 0 1px 2px rgba(16, 32, 42, .06);
  --shadow-lg: 0 1px 2px rgba(16, 32, 42, .08), 0 12px 32px -12px rgba(16, 32, 42, .28);

  /* Shape. Restrained: 8px on a card, 6px on a control or an inset box, 4px
     on small marks. The pill is for status chips only. */
  --radius-sm: 4px;
  --radius: 6px;
  --radius-lg: 8px;
  --radius-pill: 999px;

  /* Spacing, 4px steps. */
  --space-1: 4px;  --space-2: 8px;  --space-3: 12px; --space-4: 16px;
  --space-5: 20px; --space-6: 24px; --space-7: 32px; --space-8: 48px;

  /* Motion. All zero under prefers-reduced-motion (below). */
  --dur-fast: 120ms;
  --dur: 200ms;
  --dur-slow: 320ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);

  /* The institution's colour (ui.js applyBrand) is the BAND's and the seal's plate's, and nothing else's
     (2026-10-16, the console's theme; audit T6): a school's colour lightened for a dark theme made every
     primary button read as half switched off, and three primaries stood on one page. So the names below,
     which every stylesheet uses, are the accent in every theme now, with the words on it the accent's own:
       --brand-ink    text/icons in the console's colour, legible on surfaces (the selected tab's bar, a ring)
       --brand-fill   buttons, progress fills: Shawn's blue fill with dark ink in a dark theme
       --brand-soft   a tint for selected rows, active tab, chips
       --band         the header band (the school's colour exactly as given; our own ground with none) */
  --brand-ink: var(--accent);
  --brand-fill: var(--accent);
  --brand-fill-ink: var(--on-accent);
  --brand-soft: color-mix(in srgb, var(--brand-fill) 11%, var(--surface-1));
  --band: var(--brand, #0b6896);
  --on-band: var(--on-brand, #ffffff);
  --focus: var(--brand-ink);
  /* The words or the mark on a fill of a state's own colour (a step that is done, one that failed): the card's ground,
     which every theme holds at 4.5:1 or more against its ok and its danger (QA T6: white on the dark themes' light green
     was 1.53:1). Worked out on <html>, so each theme's card. */
  --on-ok: var(--surface-1);
  --on-danger: var(--surface-1);
  --topbar-h: 56px;          /* the band, at its least (64px until 2026-10-13); a square seal's 48px plate still makes 64 */

  /* Aliases (pre-foundation names). */
  --surface: var(--surface-1);
  --shadow: var(--shadow-sm);

  color-scheme: light;
}

/* ===== THEMES (2026-09-23) ================================================
   The person picks one (ui.js U.setTheme; the header's palette button and the
   sign-in page). An inline script in index.php sets it on <html> before first
   paint, so nothing flashes:
     data-theme        the theme in force: blue | light | dark | reading |
                       hologram | scholastic | dusk
     data-theme-choice what was picked (also "system", which resolves to
                       light or dark from the OS and follows it live)
     data-scheme       light | dark, for rules that only care which it is
   :root above is Light. LORE Blue is the default for everyone, and also what
   a page with no attribute gets (script blocked). Charts keep --chart-1/2/3
   (CVD-validated) in every theme.
   EVERY THEME IS THE WHOLE CONSOLE'S (2026-10-16): one choice dresses every tab,
   the faculty course page with them, so no page shows two designs. The course
   timeline's three looks are themes of the console too (Hologram, Scholastic,
   Dusk, below): each defines every token here from its own palette, the very
   colours the timeline's look has (timeline.css; the theme suite holds the two
   to one value each), and on the faculty course page the block wears the look
   of the theme in force (workspace-theme.css: LORE Blue, Light, Dark and Easy
   reading are its "console" look, the timeline's properties read from these
   tokens). */

/* DARK IS LORE BLUE (2026-10-16). Both are Shawn's palette, exactly (below), so the
   console, the faculty course page and every other tab are one design whichever of
   the two a person stored. The theme picker offers LORE Blue only (ui.js THEMES:
   Dark is `same: 'blue'`); "dark" stays a valid choice for what was stored before,
   and is what "Match my device" resolves to on a dark system. The two blocks are
   kept apart, value for value, because the suites read each by its selector. */
:root[data-theme="dark"] {
    --bg: #070d16;
    --surface-1: #0e1928;      /* darker than ui.js DARK_SURFACE (#161f26), which brand text is measured against: it reads more here */
    --surface-2: #14263a;
    --surface-3: #1c3349;
    --border: #263d53;
    --border-strong: #56738e;
    --text: #eaf3fc;
    --text-dim: #a4b8ce;
    --accent: #78d1ff;
    --accent-soft: #152c40;
    --on-accent: #062337;
    --ok: #95e0c0;       --ok-soft: #15352f;
    --warn: #f7c77b;     --warn-soft: #372c1e;
    --danger: #ff8a8f;   --danger-soft: #3a1519;
    --info: #c6b7f7;     --info-soft: #231e3c;
    --chart-1: #2E8FBB;
    --chart-2: #8B6CFF;
    --chart-3: #C07E1F;
    --shadow-sm: 0 0 0 0 transparent;
    --shadow-md: 0 1px 2px rgba(0, 0, 0, .35);
    --shadow-lg: 0 1px 2px rgba(0, 0, 0, .4), 0 16px 36px -14px rgba(0, 0, 0, .7);
    /* The header BAND keeps the school's colour as given: it is a large field
       with its own text on it. Buttons and tabs are the accent (see :root). */
    --brand-ink: var(--accent);
    --brand-fill: var(--accent);
    --brand-fill-ink: var(--on-accent);
    --brand-soft: color-mix(in srgb, var(--brand-fill) 14%, var(--surface-1));
    --band: var(--brand, #070d16);
    color-scheme: dark;
  }

/* LORE Blue: SHAWN'S PALETTE, EXACTLY (2026-10-16; his design study of the
   faculty course page, course-map-assets.html): --bg #070d16, his panel
   #0e1928 for the card, his line #263d53, his ink #eaf3fc and muted #a4b8ce,
   his blue #78d1ff, green #95e0c0, amber #f7c77b and purple #c6b7f7 as the
   accent, ok, warn and info colours. What his study has no colour for is
   derived in the same family: the two stronger fills (his node fill #14263a,
   then #1c3349), a red, and an edge for a button or a field that holds 3:1
   (#56738e: 3.6 on the card, 3.9 on the page; his own #43617c is 2.7).
   It was the student app's glacier navy, of which his palette is a cousin
   (#0A0E18 / #121C28 / #EBF6FF / #A8C2D6 / #6BD2FF), so the console and the app
   still read as one product. Measured on the card: text 15.8:1, --text-dim 8.7
   (6.4 on --surface-3), accent 10.4, ok 11.5, warn 11.3, danger 7.8, info 9.7.
   With no school's colour the header band is the page's own ground, as the
   top of his study is: the band, the tab row and the page are one ground, a
   hairline under the tabs. */
:root:not([data-theme]), :root[data-theme="blue"] {
  --bg: #070d16;             /* Shawn's --bg */
  --surface-1: #0e1928;      /* his --panel */
  --surface-2: #14263a;      /* his node fill */
  --surface-3: #1c3349;
  --border: #263d53;         /* his --line */
  --border-strong: #56738e;  /* 3.6:1 on the card: the edge of a button or a field */
  --text: #eaf3fc;           /* his --ink, 15.8:1 on the card */
  --text-dim: #a4b8ce;       /* his --muted, 8.7:1 */
  --accent: #78d1ff;         /* his --blue, 10.4:1 */
  --accent-soft: #152c40;    /* his selected lesson's ground */
  --on-accent: #062337;      /* his primary button's words: 9.5:1 on --accent */
  --ok: #95e0c0;       --ok-soft: #15352f;   /* his --green, and his asset node's ground */
  --warn: #f7c77b;     --warn-soft: #372c1e; /* his --amber, and his gap node's ground */
  --danger: #ff8a8f;   --danger-soft: #3a1519;
  --info: #c6b7f7;     --info-soft: #231e3c; /* his --purple */
  --chart-1: #2E8FBB;
  --chart-2: #8B6CFF;
  --chart-3: #C07E1F;
  --shadow-sm: 0 0 0 0 transparent;
  --shadow-md: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-lg: 0 1px 2px rgba(0, 0, 0, .45), 0 16px 36px -14px rgba(0, 0, 0, .75);
  --brand-ink: var(--accent);
  --brand-fill: var(--accent);
  --brand-fill-ink: var(--on-accent);
  --brand-soft: color-mix(in srgb, var(--brand-fill) 14%, var(--surface-1));
  --band: var(--brand, #070d16);
  color-scheme: dark;
}

/* Easy reading: warm paper, near-black ink, a deep-blue accent, Atkinson
   Hyperlegible at 18px with room between lines. Body text is AAA (18:1);
   every other text colour here is >= 6:1. Motion is off. */
:root[data-theme="reading"] {
  --font-sans: "Atkinson Hyperlegible", "Inter", ui-sans-serif, -apple-system, "Segoe UI", Arial, sans-serif;
  --fs-display: 2.25rem;
  --fs-h1: 1.75rem;
  --fs-h2: 1.4rem;
  --fs-h3: 1.2rem;
  --fs-body: 1.125rem;       /* 18px */
  --fs-small: 1rem;
  --fs-caption: .9375rem;
  --lh: 1.7;
  --lh-tight: 1.3;
  --fw-regular: 400; --fw-medium: 400; --fw-semibold: 700; --fw-bold: 700;
  --tracking-tight: 0;
  --tracking-caps: .08em;
  --bg: #FBF7EE;
  --surface-1: #FFFDF8;
  --surface-2: #F3EDE0;
  --surface-3: #E6DDCB;
  --border: #D6CCB8;
  --border-strong: #948771;
  --text: #111111;           /* 18.6:1 on the card (AAA) */
  --text-dim: #3B3830;       /* 11.5:1 */
  --accent: #0B4A8B;         /* 8.7:1 */
  --accent-soft: #E1EAF5;
  --on-accent: #ffffff;
  --ok: #1A6339;       --ok-soft: #E2EFE4;
  --warn: #6E4600;     --warn-soft: #F6E8CB;
  --danger: #921A20;   --danger-soft: #F8E0DE;
  --info: #0B4A8B;     --info-soft: #E1EAF5;
  --shadow-sm: 0 0 0 0 transparent;
  --shadow-md: 0 1px 2px rgba(60, 45, 20, .08);
  --shadow-lg: 0 1px 2px rgba(60, 45, 20, .1), 0 12px 32px -12px rgba(60, 45, 20, .28);
  --dur-fast: 0ms; --dur: 0ms; --dur-slow: 0ms;
  --brand-ink: var(--accent);
  --brand-fill: var(--accent);
  --brand-fill-ink: var(--on-accent);
  --brand-soft: color-mix(in srgb, var(--brand-fill) 10%, var(--surface-1));
  --band: var(--brand, #0B4A8B);
  --focus: #111111;          /* the plainest ring there is, on any brand */
  color-scheme: light;
}

/* THE TIMELINE'S THREE LOOKS, AS THEMES OF THE WHOLE CONSOLE (2026-10-16). Each is the
   palette its look has in timeline.css, spread over every token: the look's ground is
   --bg, its panel or card --surface-1, its ink and second ink --text and --text-dim,
   its own colour --accent, its warning, its "ok" and its struggle colour --warn, --ok
   and --info, and its faces --font-sans and --font-mono. What a look has no colour for
   (the two stronger fills, the hairline, a red, and the edge of a button or a field,
   which holds 3:1 on the card and the page) is derived from its own colour over its
   card. The theme suite (6b) measures every pair and holds each look's colours to the
   timeline's, value for value. */

/* Hologram: deep blue with a glow. Text 16.9:1 on the card, --text-dim 10.0, accent
   10.8, warn 10.5, ok 10.0, info 8.5, danger 7.3; the edge 3.6. */
:root[data-theme="hologram"] {
  --bg: #04070D;
  --surface-1: #0A1422;
  --surface-2: #101f2f;
  --surface-3: #162b3d;
  /* Its hairline is its own colour at 38% over its card, as the timeline's are (--tl-holo-line): tinted, not grey
     (2026-10-16, QA T1). And its glass, a card's ground over the lit page: the timeline's --tl-glass. */
  --border: #2f5c76;
  --look-glass: rgba(12, 26, 42, .78);
  --border-strong: #3b7391;
  --text: #EBF6FF;
  --text-dim: #A8C2D6;
  --accent: #6BD2FF;
  --accent-soft: #182f41;
  --on-accent: #03202E;
  --ok: #5BD49A;       --ok-soft: #152d32;
  --warn: #FFB454;     --warn-soft: #2a2929;
  --danger: #FF7A7A;   --danger-soft: #2a212d;
  --info: #B8A2FF;     --info-soft: #21263f;
  --chart-1: #2E8FBB;
  --chart-2: #8B6CFF;
  --chart-3: #C07E1F;
  --shadow-sm: 0 0 0 0 transparent;
  --shadow-md: 0 1px 2px rgba(0, 0, 0, .45);
  --shadow-lg: 0 1px 2px rgba(0, 0, 0, .5), 0 16px 36px -14px rgba(0, 0, 0, .8);
  --brand-ink: var(--accent);
  --brand-fill: var(--accent);
  --brand-fill-ink: var(--on-accent);
  --brand-soft: color-mix(in srgb, var(--brand-fill) 14%, var(--surface-1));
  --band: var(--brand, #04070D);
  color-scheme: dark;
}

/* Scholastic: warm paper. Source Serif 4 to read and Courier Prime for figures, the
   look's two faces (declared below). Text 15.1:1 on the card, --text-dim 8.6, accent
   11.3, warn 6.9, ok 6.3, info 7.5, danger 8.0; the edge 3.8 on the card, 3.2 on the
   page. A school's colour is read against white, as in Light. */
:root[data-theme="scholastic"] {
  --font-sans: "Source Serif 4", Georgia, "Iowan Old Style", "Palatino Linotype", "Times New Roman", serif;
  --font-mono: "Courier Prime", "Courier New", ui-monospace, monospace;
  --fw-regular: 400; --fw-medium: 500; --fw-semibold: 600; --fw-bold: 700;
  --tracking-tight: 0;
  --bg: #F1E9D8;
  --surface-1: #FFFDF6;
  --surface-2: #F7F1E4;
  --surface-3: #E6DCC6;
  --border: #DCD1BA;
  --border-strong: #847A68;   /* 3:1 on the page under its vignette too (2026-10-16; it was #8A806E, 2.92 there) */
  --text: #22252B;
  --text-dim: #474B55;
  --accent: #1F3A5F;
  --accent-soft: #ebebe8;
  --on-accent: #FBF6E9;
  --ok: #2C6A4B;       --ok-soft: #e6ebe1;
  --warn: #963F09;     --warn-soft: #f2e6da;
  --danger: #9B1C1F;   --danger-soft: #f3e2dc;
  --info: #6A3C9C;     --info-soft: #ede6eb;
  --shadow-sm: 0 0 0 0 transparent;
  --shadow-md: 0 1px 2px rgba(60, 42, 20, .10);
  --shadow-lg: 0 1px 2px rgba(60, 42, 20, .12), 0 12px 32px -12px rgba(60, 42, 20, .30);
  --brand-ink: var(--accent);
  --brand-fill: var(--accent);
  --brand-fill-ink: var(--on-accent);
  --brand-soft: color-mix(in srgb, var(--brand-fill) 10%, var(--surface-1));
  --band: var(--brand, #1F3A5F);
  color-scheme: light;
}

/* Dusk: a dark that is easy on the eyes. Warm charcoal, off-white text, one quiet sage.
   Text 11.5:1 on the card, --text-dim 7.6, accent 7.6, warn 7.8, ok 8.5, info 7.9,
   danger 7.5; the edge 3.5. */
:root[data-theme="dusk"] {
  --bg: #1B1D21;
  --surface-1: #24272C;
  --surface-2: #2A2D33;
  --surface-3: #30343B;
  /* Its hairline is its sage at 30% over its card, as the timeline's are (--tl-holo-line); a card keeps the matte card's
     own edge, #3E424A (looks.css, --look-card-edge). 2026-10-16, QA T3. */
  --border: #485652;
  --border-strong: #767b84;
  --text: #E4E1D9;
  --text-dim: #BCB9B0;
  --accent: #9CC2A9;
  --accent-soft: #353d3e;
  --on-accent: #1B1D21;
  --ok: #A9CBB7;       --ok-soft: #353c3e;
  --warn: #E2B36B;     --warn-soft: #3d3934;
  --danger: #F2A3A3;   --danger-soft: #3f373b;
  --info: #CDB2E1;     --info-soft: #3a3944;
  --chart-1: #2E8FBB;
  --chart-2: #8B6CFF;
  --chart-3: #C07E1F;
  --shadow-sm: 0 0 0 0 transparent;
  --shadow-md: 0 1px 2px rgba(0, 0, 0, .35);
  --shadow-lg: 0 1px 2px rgba(0, 0, 0, .4), 0 16px 36px -14px rgba(0, 0, 0, .65);
  --brand-ink: var(--accent);
  --brand-fill: var(--accent);
  --brand-fill-ink: var(--on-accent);
  --brand-soft: color-mix(in srgb, var(--brand-fill) 14%, var(--surface-1));
  --band: var(--brand, #1B1D21);
  color-scheme: dark;
}

@media (prefers-reduced-motion: reduce) {
  :root { --dur-fast: 0ms; --dur: 0ms; --dur-slow: 0ms; }
}

@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: normal; font-weight: 400; font-display: swap;
  src: url("../fonts/atkinson-400.woff2?v=1") format("woff2");
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: normal; font-weight: 700; font-display: swap;
  src: url("../fonts/atkinson-700.woff2?v=1") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/inter-var.woff2?v=1") format("woff2");
}
/* The Scholastic theme's two faces (2026-10-16), the same files timeline.css declares for its look (each under the
   SIL Open Font License, beside it in console/fonts). A browser fetches a face only when something is set in it. */
@font-face {
  font-family: "Source Serif 4";
  font-style: normal; font-weight: 400 700; font-display: swap;
  src: url("../fonts/source-serif-4-var.woff2?v=1") format("woff2");
}
@font-face {
  font-family: "Courier Prime";
  font-style: normal; font-weight: 400; font-display: swap;
  src: url("../fonts/courier-prime-400.woff2?v=1") format("woff2");
}
@font-face {
  font-family: "Courier Prime";
  font-style: normal; font-weight: 700; font-display: swap;
  src: url("../fonts/courier-prime-700.woff2?v=1") format("woff2");
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: var(--lh);
  font-weight: var(--fw-regular);
  /* Contextual alternates. (Inter's cv05 and cv08, its disambiguated l and I, went with Inter in 2026-10-16: Atkinson
     Hyperlegible draws those letters apart in its own shapes.) */
  font-feature-settings: "calt";
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
::selection { background: color-mix(in srgb, var(--brand-fill) 28%, transparent); }

/* Panels are toggled with the `hidden` attribute. Flex/grid display rules
 * override it unless this is stated, which cost an hour once. */
[hidden] { display: none !important; }

h1, h2, h3, h4 { margin: 0 0 .4em; line-height: var(--lh-tight); font-weight: var(--fw-semibold); letter-spacing: var(--tracking-tight); }
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); letter-spacing: -.01em; }
p { margin: 0 0 .8em; }
a { color: var(--accent); }

.muted { color: var(--text-dim); }
.small { font-size: var(--fs-small); }
.mono { font-family: var(--font-mono); font-size: .9em; }

/* Type scale as classes, for tabs that build their own headings. */
.t-display { font-size: var(--fs-display); line-height: 1.1; font-weight: var(--fw-bold); letter-spacing: -.025em; }
.t-h1 { font-size: var(--fs-h1); line-height: var(--lh-tight); font-weight: var(--fw-semibold); letter-spacing: var(--tracking-tight); }
.t-h2 { font-size: var(--fs-h2); line-height: var(--lh-tight); font-weight: var(--fw-semibold); letter-spacing: var(--tracking-tight); }
.t-h3 { font-size: var(--fs-h3); line-height: var(--lh-tight); font-weight: var(--fw-semibold); }
.t-body { font-size: var(--fs-body); }
.t-small { font-size: var(--fs-small); }
.t-caption { font-size: var(--fs-caption); letter-spacing: .01em; }
.t-eyebrow { font-size: var(--fs-caption); font-weight: var(--fw-bold); text-transform: uppercase;
             letter-spacing: var(--tracking-caps); color: var(--text-dim); }
/* Figures line up in columns and do not jitter as they change. */
.tnum, .num, td.num, .stat-value { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
/* The figures the other stylesheets draw (cards.css, media.css, charts.css)
   that did not ask for it themselves: every statistic sets in tabular
   numerals, so a count does not shift as it changes. */
.sc-score-value, .sc-progress-head, .sc-when, .lb-avg, .lc-chip-value, .cc-meta, .cc-whenv { font-variant-numeric: tabular-nums; }
.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; }
.right { text-align: right; }
.nowrap { white-space: nowrap; }
.stack > * + * { margin-top: .75rem; }
.row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
.grow { flex: 1 1 auto; min-width: 0; }   /* may shrink below its content: a long <select> option must not push past the card */

/* --- boot / sign-in ------------------------------------------------------ */

.boot, .signin {
  min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 2rem 1rem;
}
/* The sign-in page is a column: the card, then the row of themes under it, in the page's flow (fix round 1, L4). The row
   stood at the foot of the window over the page: with seven themes it took three rows on a phone, overlapped the card
   at 375 by 667 and at 360 by 640, and its last row was below the fold. */
.signin { flex-direction: column; gap: var(--space-5); }
.boot-inner { text-align: center; color: var(--text-dim); font-size: var(--fs-small); }
.spinner {
  width: 28px; height: 28px; margin: 0 auto .8rem;
  border: 3px solid var(--surface-3); border-top-color: var(--brand-fill);
  border-radius: 50%; animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 3s; } }

/* ONE WAY TO SAY "WORKING" AND "DONE" (2026-10-31, ui.js busy/loading/result). The ring is in the colour of the words
   it stands beside, so it is right on a primary button, a quiet one and a line of text in every theme. It shows after
   0.3 s (.spin-later: a fast answer never flashes one) and the still-working words after 10 s, both by CSS, so no
   timer is left running; the words are hidden by `visibility`, which a screen reader honours, so they are not read out
   before they show. With reduced motion the ring stands still and the words say it all. */
.spin-inline { display: inline-block; flex: none; width: 1em; height: 1em; vertical-align: -.15em; margin-right: .4em; }
.spin-inline::before {
  content: ""; display: block; box-sizing: border-box; width: 100%; height: 100%;
  border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%;
  animation: lore-spin .8s linear infinite;
}
.spin-later { opacity: 0; animation: lore-appear 0s linear .3s forwards; }
@keyframes lore-appear { to { opacity: 1; } }
@keyframes lore-reveal { to { visibility: visible; max-height: 6em; } }
.loading-slow { flex-basis: 100%; }   /* added by U.loading at 10 s, so a screen reader reads it out */
/* Put in place at 10 s by U.busy (a hidden line would still take its width and the row's gap). */
.busy-slow { align-self: center; font-size: var(--fs-small); color: var(--text-dim); }
/* A button's ring takes no width until it shows, so a fast answer does not widen the button it was pressed on. */
.busy-ring.spin-later { width: 0; margin-right: 0; animation: lore-grow 0s linear .3s forwards; }
@keyframes lore-grow { to { opacity: 1; width: 1em; margin-right: .4em; } }
/* A line above a list that is read again: hidden and with no height until 0.3 s, so a quick answer moves nothing. */
.loading-line.loading-later { visibility: hidden; max-height: 0; overflow: hidden; animation: lore-reveal 0s linear .3s forwards; }
.loading-line { display: flex; flex-wrap: wrap; align-items: center; gap: .25rem .5rem; color: var(--text-dim); font-size: var(--fs-small); line-height: 1.45; }
.loading-line .spin-inline { margin-right: 0; }
.loading-line .loading-icon { display: inline-flex; flex: none; }
.loading-line.is-done .loading-icon { color: var(--ok); }
.loading-line.is-failed { color: var(--text); }
.loading-line.is-failed .loading-icon { color: var(--danger); }
.result-line { display: flex; align-items: flex-start; gap: .4rem; margin-top: .4rem; color: var(--text); font-size: var(--fs-small); line-height: 1.45; }
.result-line .result-icon { display: inline-flex; flex: none; margin-top: 2px; }
.result-ok .result-icon { color: var(--ok); }
.result-error .result-icon { color: var(--danger); }
@media (prefers-reduced-motion: reduce) { .spin-inline::before { animation: none; } }

/* The sign-in page: the one screen every professor sees first. The plain page
   colour behind a single bordered card; no photograph, no marketing, no glow
   (2026-10-11). */
.signin {
  position: relative; isolation: isolate; overflow: hidden;
  background: var(--bg);
}
.signin-card { width: 100%; max-width: 400px; padding: var(--space-7) var(--space-7) var(--space-6); border-radius: var(--radius-lg);
               box-shadow: var(--shadow-md); animation: rise var(--dur-slow) var(--ease-out) both; }
.signin-mark { width: 40px; height: 40px; border-radius: var(--radius); display: inline-flex; align-items: center; justify-content: center;
               color: var(--on-band); margin-bottom: var(--space-4);
               background: var(--band); }
.signin-mark .icon { width: 22px; height: 22px; }
.signin-card h1.brand { font-size: var(--fs-h1); letter-spacing: .08em; }
.signin-title { font-size: var(--fs-h2); font-weight: var(--fw-semibold); margin: .15rem 0 .1rem; letter-spacing: var(--tracking-tight); }
.signin-card > .muted:first-of-type { margin-bottom: .6rem; }
.signin-card label { display: block; margin: 1rem 0 .35rem; font-weight: var(--fw-semibold); font-size: var(--fs-small); }
.signin-card .btn { width: 100%; margin-top: 1.25rem; padding-block: .7rem; font-size: var(--fs-body); }
.signin-card input { width: 100%; padding: .6rem .75rem; }
/* The eye sits inside the password field, at its right; the field leaves it room. */
.signin-card .pw-field { position: relative; }
.signin-card .pw-field input { padding-right: 3rem; }
.signin-card .pw-toggle { position: absolute; top: 0; right: 0; bottom: 0; width: 2.75rem; min-height: 24px; display: inline-flex;
  align-items: center; justify-content: center; padding: 0; border: 0; border-radius: var(--radius-sm); background: transparent;
  color: var(--text-dim); cursor: pointer; }
.signin-card .pw-toggle:hover, .signin-card .pw-toggle[aria-pressed="true"] { color: var(--text); }
.signin-card .pw-toggle:focus-visible { outline-offset: -4px; }
.signin-card .signin-forgot { margin: 1rem 0 .5rem; }
.signin-card .form-error { margin: .75rem 0 0; padding: .55rem .75rem; border-radius: var(--radius-sm);
                           background: var(--danger-soft); border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent); }
.signin-card .muted.small:last-child { border-top: 1px solid var(--border); padding-top: .9rem; margin: .9rem 0 0; }
.brand { font-size: 1.4rem; font-weight: 750; letter-spacing: .06em; color: var(--brand-ink); margin: 0; }
@keyframes rise { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

/* --- chrome -------------------------------------------------------------- */

/* THE BAND. The school's colour exactly as it gave it, as a field rather than
   a 4px stripe, with its own legible text (--on-band is black or white,
   whichever reads). The seal sits on its white plate at the left. One flat
   colour (2026-10-11): a highlight and a fade over it made every school's
   band the same generic gradient, and darkened the colour it had given us.
   Slimmer (2026-10-13): 56px at least, and .45rem above and below, so the 40px
   plate of a wordmark or a monogram, with the hairline under the band, is
   still inside that (40 + 14.4 + 1). */
.topbar {
  --gutter: max(1.5rem, calc((100% - 1400px) / 2 + 1.5rem));
  --band-ctl: 34px;   /* a control on the band: the palette button and Sign out are one height */
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  min-height: var(--topbar-h); padding: .45rem var(--gutter);
  color: var(--on-band);
  background: var(--band);
  border-bottom: 1px solid color-mix(in srgb, var(--band) 60%, #000);
}
.topbar-left, .topbar-right { display: flex; align-items: center; gap: .85rem; min-width: 0; }
.topbar .brand { font-size: 1.05rem; color: var(--on-band); }
.scope-block { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
.scope-name { font-weight: var(--fw-bold); font-size: 1.08rem; color: var(--on-band); letter-spacing: -.01em;
              white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.console-name { font-size: var(--fs-caption); color: color-mix(in srgb, var(--on-band) 74%, transparent); letter-spacing: .02em; }
.console-name:empty { display: none; }
.who-block { display: flex; align-items: center; gap: .55rem; min-width: 0; }
/* WHO IS SIGNED IN (2026-10-13). One sentence in two weights (app.js writes the two spans): the name in the
   band's full text colour, semibold; what they are after it a little dimmer, at the ordinary weight. It was
   one string of one weight at 86%. The avatar has no ring: it is a coloured disc on a coloured band already.
   THE ROLE IS 88% OF THE BAND'S TEXT COLOUR, AND THE WEIGHT IS WHAT TELLS THE TWO APART. A band is a school's
   colour as given, and on a mid-tone one white has little to spare: at 74% the role read at 3.7 to 1 on a red
   (#C8102E) and on a green (#007A33), and at 86% still 4.47 on that green. 88 is 4.61 there and 4.77 on the
   red; test_console_nav_ui.js works it out for those two, a navy and the four bands with no school. */
.who { color: var(--on-band); font-size: var(--fs-small); white-space: nowrap;
       overflow: hidden; text-overflow: ellipsis; max-width: 32ch; }
.who-name { font-weight: var(--fw-semibold); }
.who-role { color: color-mix(in srgb, var(--on-band) 88%, transparent); }
.who-avatar:empty { display: none; }
/* The palette button and Sign out are one shape: one height, one radius, one hairline. They were two boxes
   of 33px and 28px. Hover and the keyboard's ring are drawn in --on-band, black or white as the school's
   colour needs, so both show on a band of any colour. */
.topbar .btn-band { height: var(--band-ctl); padding-block: 0; border-radius: var(--radius); color: var(--on-band);
                    background: color-mix(in srgb, var(--on-band) 10%, transparent);
                    border: 1px solid color-mix(in srgb, var(--on-band) 24%, transparent); }
.topbar .btn-band.btn-icon { width: var(--band-ctl); padding-inline: 0; }
.topbar .btn-band:hover { background: color-mix(in srgb, var(--on-band) 18%, transparent); color: var(--on-band);
                          border-color: color-mix(in srgb, var(--on-band) 45%, transparent); }
.topbar :focus-visible { outline-color: var(--on-band); }

/* Tabs stay in reach: they stick to the top once the band scrolls away. */
.tabs {
  --gutter: max(1.5rem, calc((100% - 1400px) / 2 + 1.5rem));
  --fade: 1.5rem;   /* a cut edge fades over this much: never more than the page's gutter while the row sticks (.tabs[data-more]) */
  position: sticky; top: 0; z-index: 20;
  display: flex; gap: .15rem; padding: .35rem var(--gutter) 0;   /* the bar's height is what --tabs-h's 49px fallback assumes */
  /* Solid: no frosted glass, no shadow, a hairline under it. On the page's own ground (2026-10-16), as the tabs of
     Shawn's study stand on his page: with no school's colour, the band above, this row and the page are one ground. */
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  overflow-x: auto; scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  --tab-px: .85rem;                    /* the room at each side of a tab */
  --tab-lead: calc(17px + .45rem);     /* its icon and the gap after it: what the underline steps over */
  position: relative; appearance: none; background: none; border: 0;
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .6rem var(--tab-px) .75rem; font: inherit; font-size: var(--fs-small); font-weight: var(--fw-semibold); color: var(--text-dim);
  cursor: pointer; border-radius: var(--radius-sm) var(--radius-sm) 0 0; white-space: nowrap; flex: 0 0 auto;
  transition: color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}
.tab .icon { width: 17px; height: 17px; opacity: .8; }
/* The underline is under the LABEL (2026-10-13): from where the words start to where they end, not under
   the icon and the gap as well. It is --brand-ink, which is the accent in every theme since 2026-10-16 (a
   school's colour is the band's alone), and 3px, as the bar under the view showing on the course page and
   under a house filter in force: ONE TAB SYSTEM, Shawn's (his tabs have a 3px bar). */
.tab::after { content: ""; position: absolute; left: calc(var(--tab-px) + var(--tab-lead)); right: var(--tab-px); bottom: 0; height: 3px;
              background: var(--brand-ink); transform: scaleX(0);
              transition: transform var(--dur) var(--ease-out); }
.tab:hover { color: var(--text); background: var(--surface-2); }
/* The selected tab is the BRIGHTEST text on the bar (2026-09-24): full
   --text, above the others' --text-dim, in every theme. The school's colour
   stays on its underline and icon. It used to be --brand-ink, which in Dark
   read 4.44:1 on the translucent bar and was dimmer than its neighbours. */
.tab[aria-selected="true"] { color: var(--text); }
.tab[aria-selected="true"] .icon { opacity: 1; color: var(--brand-ink); }
.tab[aria-selected="true"]::after { transform: scaleX(1); }

/* TWO GROUPS (2026-10-13). The five tabs only Didactical's staff have (index.php: data-group="house") stand
   at the right end of the row behind a hairline, without icons, so the row reads as the six places a teacher
   works in and then ours, not as eleven of a kind. Unselected they are --text-dim at the same size as the
   rest (the .tab rule above); selected, a house tab is --text with the underline like any other, so nothing
   here sets a colour. "The first one that is not hidden" is every visible house tab, with the ones that
   follow a visible one put back: a teacher has none, so no rule here matches and no hairline is drawn. */
.tab[data-group="house"] { --tab-lead: 0px; }
.tab[data-group="house"] .icon { display: none; }
.tab[data-group="house"]:not([hidden]) { margin-left: auto; }
.tab[data-group="house"]:not([hidden]) ~ .tab[data-group="house"] { margin-left: 0; }
.tab[data-group="house"]:not([hidden])::before {
  content: ""; position: absolute; left: calc(-.5rem - .5px); top: .6rem; width: 1px;
  height: calc(1em * var(--lh));   /* as tall as the line of the label beside it, in any theme's type */
  background: var(--border-strong);
}
.tab[data-group="house"]:not([hidden]) ~ .tab[data-group="house"]::before { content: none; }
/* Room for the hairline when the row is full and margin-left: auto has nothing to give: the tab before the
   group stands 1rem off it, and the hairline is in the middle of that. Without :has() the hairline sits
   over the end of that tab's padding instead, .5rem clear of its label. */
.tab:not([data-group]):has(+ .tab[data-group="house"]:not([hidden])) { margin-right: .85rem; }

/* THE ROW NEVER CUTS A WORD WITHOUT SAYING SO (2026-10-13). app.js measures the row and sets, on nav#tabs:
     data-tight="1"   the tabs do not fit with their icons: labels only, a little closer together;
     data-more        a tab is still cut off: at the "right" edge, the "left", or "both", as the row is
                      scrolled. That edge fades, so a cut label is visibly the start of more and not the end
                      of the row. (Only a tab counts: the row's own gutter out of sight cuts no word.)
   At any width. The fade used to be a phone-only rule that was always on, with or without anything beyond it.
   THE FADE IS AS WIDE AS THE PAGE'S GUTTER AND NO WIDER WHERE THE ROW STICKS (--fade, on .tabs above). A mask
   makes that edge of the row see-through, and a row stuck to the top of the window has the page scrolling
   under it: at 2.5rem the corner of a course's cover showed through the end of the row. Inside the gutter
   (the 1.5rem of .panel, .stats and .build-banner) there is only ever the page's ground. On a phone the row
   does not stick and the fade is the 2.5rem it has been (the phone rules, below). */
.tabs[data-tight="1"] .tab { --tab-px: .65rem; --tab-lead: 0px; }
.tabs[data-tight="1"] .tab .icon { display: none; }
.tabs[data-more="right"] { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent);
                           mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent); }
.tabs[data-more="left"] { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - var(--fade)), transparent);
                          mask-image: linear-gradient(to left, #000 calc(100% - var(--fade)), transparent); }
.tabs[data-more="both"] { -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent);
                          mask-image: linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent); }

/* One focus ring for everything: the school's colour, offset so it never
   sits on top of the control's own border. */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.tab:focus-visible { outline-offset: -2px; }

.stats { --gutter: max(1.5rem, calc((100% - 1400px) / 2 + 1.5rem));
         display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--space-3);
         padding: var(--space-6) var(--gutter) 0; }
/* .stat tiles: see "design foundation" at the end of this file. */

.panel { padding: 1.5rem; max-width: 1400px; margin-inline: auto; animation: fade-in var(--dur) var(--ease) both; }

/* --- cards, tables ------------------------------------------------------- */

.card {
  background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-5) var(--space-6); box-shadow: var(--shadow-sm);
}
.card + .card { margin-top: var(--space-4); }
.card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4);
             margin-bottom: var(--space-4); flex-wrap: wrap; }
.card-head select { max-width: 100%; min-width: 0; }
/* Headings carry a bottom margin for prose; inside a head row that margin is
 * part of the flex item, so centring would sit the heading high and the
 * buttons low. The row supplies its own spacing. */
.card-head > h1, .card-head > h2, .card-head > h3,
.modal-head > h1, .modal-head > h2, .modal-head > h3 { margin: 0; }

.table-wrap { overflow-x: auto; margin-inline: -.25rem; }
table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--fs-small); }
th, td { text-align: left; padding: .7rem .75rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
th { font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-dim);
     font-weight: var(--fw-semibold); padding-block: .55rem; background: var(--surface-1); white-space: nowrap; }
tbody tr:last-child > td, tbody tr:last-child > th { border-bottom: 0; }   /* a row's own heading too (2026-10-16, QA T11) */
tbody tr { transition: background-color var(--dur-fast) var(--ease); }
tbody tr:hover { background: var(--surface-2); }
tbody tr.clickable:hover { background: color-mix(in srgb, var(--brand-fill) 6%, var(--surface-1)); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
.clickable { cursor: pointer; }
/* Rows are reachable by keyboard (U.activateRow), so the focus ring is not
   optional — a tab stop nobody can see is not an improvement. */
.clickable:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
/* Sticky column heads on a wide screen, under the sticky tabs. Only there:
   sticky needs the wrapper not to be a scroll container, and a phone stacks
   rows as cards anyway. */
@media (min-width: 1100px) {
  .table-wrap { overflow-x: visible; }
  thead th { position: sticky; top: var(--tabs-h, 49px); z-index: 2; box-shadow: inset 0 -1px 0 var(--border); }
}

.empty { padding: var(--space-4) 0; color: var(--text-dim); }   /* laid out with the design foundation, below */

/* --- controls ------------------------------------------------------------ */

/* Flat (2026-10-11): a button is a bordered box, the primary one a plain fill
   of the school's colour. No sheen and no coloured glow under it; the fill is
   the emphasis. */
.btn {
  appearance: none; font: inherit; font-size: var(--fs-small); font-weight: var(--fw-semibold); cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--surface-1); color: var(--text);
  padding: .5rem .9rem; border-radius: var(--radius); line-height: 1.25; white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease), filter var(--dur-fast) var(--ease);
}
.btn:hover { background: var(--surface-2); border-color: color-mix(in srgb, var(--text) 30%, var(--border-strong)); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn-primary { background: var(--brand-fill); border-color: transparent; color: var(--brand-fill-ink); }
.btn-primary:hover { background-color: var(--brand-fill); border-color: transparent; filter: brightness(1.07) saturate(1.05); }
.btn-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: var(--danger-soft); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 14%, var(--surface-1)); border-color: var(--danger); }
.btn-quiet { border-color: transparent; background: none; color: var(--text-dim); box-shadow: none; }
.btn-quiet:hover { background: var(--surface-2); color: var(--text); border-color: transparent; }
.btn-sm { padding: .3rem .6rem; font-size: var(--fs-caption); }
.btn-icon { padding: .45rem; }

input, select, textarea {
  font: inherit; font-size: var(--fs-body); color: var(--text); background: var(--surface-1);
  border: 1px solid var(--border-strong); border-radius: var(--radius); padding: .5rem .65rem; max-width: 100%;
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
input:hover, select:hover, textarea:hover { border-color: color-mix(in srgb, var(--text) 35%, var(--border-strong)); }
/* The ring is a shadow (it follows the radius); the transparent outline is
   what Windows High Contrast paints instead, since it drops shadows. */
input:focus-visible, select:focus-visible, textarea:focus-visible,
input:focus, select:focus, textarea:focus {
  outline: 2px solid transparent; border-color: var(--brand-fill);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-fill) 24%, transparent);
}
input[type=checkbox], input[type=radio] { accent-color: var(--brand-fill); box-shadow: none; }
input::placeholder, textarea::placeholder { color: var(--text-dim); }   /* plain: at 80% it was 3.8:1 in Light (audit K3) */
textarea { width: 100%; min-height: 8rem; resize: vertical; font-family: var(--font-mono); font-size: .88rem; line-height: 1.5; }
label.field { display: block; margin-bottom: 1rem; }
label.field > span { display: block; font-weight: var(--fw-semibold); font-size: var(--fs-small); margin-bottom: .35rem; }
label.field > .small.muted { margin-top: .3rem; }
.form-error { color: var(--danger); font-size: var(--fs-small); }

/* .badge / .chip: see "design foundation" at the end of this file. */

/* A value the admin must copy now because it is never shown again. */
.secret {
  display: flex; gap: .5rem; align-items: center; padding: .65rem .75rem;
  background: var(--accent-soft); border: 1px dashed var(--accent); border-radius: var(--radius);
  word-break: break-all;
}

/* --- modal --------------------------------------------------------------- */

.modal-backdrop {
  position: fixed; inset: 0; background: rgba(8, 16, 22, .7);   /* a plain dim, no blur */
  display: flex; align-items: flex-start; justify-content: center; padding: 6vh 1rem; z-index: 50;
  overflow-y: auto; animation: fade-in var(--dur) var(--ease) both;
}
.modal { width: 100%; max-width: 640px; padding: 0; border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
         animation: rise var(--dur-slow) var(--ease-out) both; overflow: hidden; }
.modal.wide { max-width: 960px; }
.modal-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-4);
              padding: var(--space-4) var(--space-4) var(--space-3) var(--space-6); margin: 0; }
.modal-head h2 { font-size: var(--fs-h2); }
.modal-body { padding: 0 var(--space-6) var(--space-5); }
.modal-foot { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: var(--space-2); margin: 0;
              padding: var(--space-3) var(--space-6); background: var(--surface-2); border-top: 1px solid var(--border); }
.modal-close { padding: .4rem; }

/* --- toasts -------------------------------------------------------------- */

.toasts { position: fixed; right: 1rem; bottom: 1rem; display: flex; flex-direction: column; gap: .5rem; z-index: 60;
          max-width: calc(100vw - 2rem); }
.toast {
  display: flex; align-items: flex-start; gap: .6rem;
  background: var(--surface-1); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius);
  padding: .7rem .9rem; box-shadow: var(--shadow-lg); max-width: 400px; font-size: var(--fs-small); line-height: 1.45;
  animation: toast-in var(--dur-slow) var(--ease-out) both;
}
/* Over a dialog (QA F8, ui.js toastsClear): the stack leaves the bottom right, where a dialog's buttons are, for the top
 * left, where it has a title and nothing to press. The phone rule below, of the same weight, restores the full width. */
.toasts[data-over-modal] { top: 1rem; bottom: auto; right: auto; left: 1rem; }
.toast .toast-icon { color: var(--info); margin-top: 1px; }
.toast.error .toast-icon, .toast.danger .toast-icon { color: var(--danger); }
.toast.ok .toast-icon { color: var(--ok); }
/* `warn` is raised for things that succeeded but need a human to look
 * ("that section has no students yet"). Left on the accent colour it read as
 * a plain success. `info` keeps the default deliberately. */
.toast.warn .toast-icon { color: var(--warn); }
.toast.error, .toast.danger { border-color: color-mix(in srgb, var(--danger) 40%, var(--border)); }
@keyframes toast-in { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: none; } }

/* --- pipeline / studio --------------------------------------------------- */

/* The build page's note that nobody can open the published course yet (QA F5): a thing to do, so the warning colour. */
.audience-note { color: var(--warn); font-weight: var(--fw-semibold); margin: .4rem 0 0; }
.pipeline { display: flex; flex-wrap: wrap; gap: .35rem; }
.stage {
  border: 1px solid var(--border); border-radius: 999px; padding: .18rem .6rem;
  font-size: .78rem; font-weight: 600; color: var(--text-dim); background: var(--surface-2);
}
.stage[data-status="running"] { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.stage[data-status="done"] { border-color: var(--ok); color: var(--ok); background: var(--ok-soft); }
.stage[data-status="failed"] { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); }
/* `blocked` is the review gate waiting on a person — the one chip that means
 * "it is your turn", so it must not look like a stage that has not started.
 * `skipped` is the opposite: deliberately not applicable, so it recedes.
 * border-COLOR only, so the dashed gate border below survives. */
.stage[data-status="blocked"] { border-color: var(--warn); color: var(--warn); background: var(--warn-soft); }
.stage[data-status="skipped"] { opacity: .6; }
.stage[data-gate="1"] { border-style: dashed; }

.module { border: 1px solid var(--border); border-radius: var(--radius); padding: .8rem .9rem; }
.module + .module { margin-top: .6rem; }
.module[data-review="approved"] { border-left: 4px solid var(--ok); }
.module[data-review="rejected"] { border-left: 4px solid var(--danger); }
.module[data-review="pending_review"] { border-left: 4px solid var(--warn); }

.bar { height: 8px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.bar > span { display: block; height: 100%; background: var(--accent); }

/* --- charts -------------------------------------------------------------- */

.chart { width: 100%; overflow-x: auto; }
.chart svg { display: block; }
.chart text { fill: var(--text-dim); font-size: 11px; }
.chart .label-strong { fill: var(--text); font-weight: 600; }
.legend { display: flex; gap: 1rem; flex-wrap: wrap; font-size: .82rem; color: var(--text-dim); }
.legend .key { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: .35rem; }


/* --- live build progress (studio, 2026-09-11) ----------------------------- */
.progress-card .now { margin: .2rem 0 .7rem; font-size: 1.02rem; font-weight: 600; color: var(--text); }
.progress-card .now[data-tone="ok"] { color: var(--ok); }
.progress-card .now[data-tone="warn"] { color: var(--warn); }
.progress-card .now[data-tone="danger"] { color: var(--danger); }
.progress-card .now[data-tone="muted"] { color: var(--text-dim); font-weight: 500; }
.bar.bar-lg { position: relative; height: 14px; margin-bottom: .9rem; }
.bar.bar-lg > b { position: absolute; right: 8px; top: -1px; font-size: 11px; color: var(--text-dim); }
.bar.bar-sm { height: 6px; margin: .25rem 0; position: relative; }
.bar .bar-active { position: absolute; top: 0; height: 100%; background: var(--accent); opacity: .35; }
.steps { display: flex; flex-direction: column; gap: .15rem; }
.step { display: grid; grid-template-columns: 26px 1fr minmax(120px, 34%); gap: .6rem; align-items: center;
        padding: .45rem .3rem; border-top: 1px solid var(--border); }
.step:first-child { border-top: 0; }
.step[data-status="pending"] .step-label { color: var(--text-dim); }
.step[data-status="running"] .step-label { color: var(--accent); }
.step[data-status="failed"] .step-label { color: var(--danger); }
/* Blocked is a person's turn, not a failure — the chip strip already says so. */
.step[data-status="blocked"] .step-label { color: var(--warn); }
.step[data-status="skipped"] .step-label { color: var(--text-dim); }
.step[data-status="skipped"] { opacity: .75; }
.step-label { font-weight: 600; font-size: .95rem; }
.step-right { text-align: right; }
.step-progress { display: flex; flex-direction: column; align-items: flex-end; }
.step-progress .bar { width: 100%; }
.step-icon { width: 22px; height: 22px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
             font-size: 12px; font-weight: 700; border: 1.5px solid var(--border); color: var(--text-dim); position: relative; }
.step-icon[data-status="done"] { background: var(--ok); border-color: var(--ok); color: var(--on-ok); }
.step-icon[data-status="failed"] { background: var(--danger); border-color: var(--danger); color: var(--on-danger); }
.step-icon[data-status="blocked"] { border-color: var(--warn); color: var(--warn); }
.step-icon[data-status="running"] { border-color: var(--accent); }
/* Scoped to the step icon it lives in: unscoped, this rule also matched the
 * full-page boot spinner and absolutely positioned it into the viewport
 * corner. studio.js only ever appends it inside .step-icon. */
.step-icon .spinner { position: absolute; inset: 2px; width: auto; height: auto; margin: 0;
           border-radius: 50%; border: 2px solid transparent;
           border-top-color: var(--accent); animation: lore-spin .9s linear infinite; }
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); display: inline-block; margin-right: .4rem;
            animation: lore-pulse 1.6s ease-in-out infinite; }
@keyframes lore-spin { to { transform: rotate(360deg); } }
@keyframes lore-pulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .step-icon .spinner, .live-dot { animation: none; } }
@media (max-width: 640px) { .step { grid-template-columns: 26px 1fr; } .step-right { grid-column: 2; text-align: left; }
  .step-progress { align-items: flex-start; } }

/* --- the build, as the teacher sees it (2026-09-24) -------------------------
 * No percentage: one sentence of what is happening now, counts per step, and
 * when the writing is done a card that cannot be mistaken for "still busy". */
.progress-card .build-headline { font-size: 1.25rem; font-weight: 700; margin: .1rem 0 .25rem; letter-spacing: var(--tracking-tight); }
.progress-card .build-sub { margin: 0 0 .8rem; color: var(--text-dim); }
.progress-card .build-headline + .steps { margin-top: .7rem; }
.build-timing { font-variant-numeric: tabular-nums; }
.build-foot { display: flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: center; justify-content: space-between;
              margin-top: .8rem; padding-top: .7rem; border-top: 1px solid var(--border); }
.step[data-status="skipped"] .step-body .small { opacity: 1; }
.step-icon[data-status="skipped"] { border-style: dashed; }

.ready-card { border: 1px solid var(--ok); box-shadow: inset 4px 0 0 var(--ok); }   /* a green rule down the edge, no wash */
.ready-head { display: flex; gap: .9rem; align-items: flex-start; }
.ready-mark { color: var(--ok); flex: none; margin-top: .1rem; }
.ready-title { margin: 0 0 .2rem; font-size: 1.35rem; letter-spacing: var(--tracking-tight); }
.ready-counts { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0 1.1rem; }
.ready-count { display: flex; flex-direction: column; min-width: 7.5rem; padding: .55rem .8rem;
               border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-1); }
.ready-count b { font-size: 1.5rem; line-height: 1.1; font-variant-numeric: tabular-nums; }
.ready-count span { font-size: .82rem; color: var(--text-dim); }
.ready-actions { align-items: center; gap: .8rem; flex-wrap: wrap; }
.btn.btn-lg { padding: .65rem 1.2rem; font-size: 1rem; }
.ready-note { margin: .7rem 0 0; }
.build-how { margin-top: 1rem; }
.build-how summary { cursor: pointer; color: var(--text-dim); font-size: .88rem; }
.build-how .steps { margin-top: .5rem; }
.build-cell .badge { margin-bottom: .2rem; }
/* On a phone the cells of a table are stacked rows of label and value (below); the status cell of the Build a course list
   holds a chip and a sentence, which then stood edge to edge: there they stand one over the other (audit L1). */
@media (max-width: 700px) {
  .table-wrap td.build-cell { flex-direction: column; align-items: flex-end; gap: .2rem; text-align: right; }
  .table-wrap td.build-cell .badge { margin-bottom: 0; }
}

/* The console-wide "it's ready" line, above whichever tab is open. */
/* The open demo's band (2026-10-15): a demo account only (app.js). */
.demo-band { max-width: 1400px; margin: 1.1rem auto 0; padding: 0 1.5rem; }
.demo-band[hidden] { display: none; }
.demo-band-inner { padding: .6rem .9rem; border: 1px solid var(--info); border-radius: var(--radius);
                   background: var(--info-soft); color: var(--text); font-size: var(--fs-small); }
.build-banner { max-width: 1400px; margin: 1.1rem auto 0; padding: 0 1.5rem; }
.build-banner[hidden] { display: none; }
.build-banner-inner { display: flex; gap: .8rem; align-items: center; padding: .7rem .9rem;
                      border: 1px solid var(--ok); border-radius: var(--radius); background: var(--ok-soft); }
.build-banner-inner[data-tone="danger"] { border-color: var(--danger); background: var(--danger-soft); }
.build-banner-mark { color: var(--ok); display: inline-flex; }
.build-banner-inner[data-tone="danger"] .build-banner-mark { color: var(--danger); }
.build-banner-text { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.build-banner-text .small { color: var(--text-dim); }
@media (max-width: 640px) { .build-banner-inner { flex-wrap: wrap; } .ready-count { min-width: 6rem; } }

/* --- lesson editor (courses, 2026-09-11) ---------------------------------- */
.question-card { margin-top: .6rem; background: var(--surface-2, transparent); }
.question-card textarea { font-family: inherit; font-size: .95rem; min-height: 3.2rem; }
.question-card input[type=text] { width: 100%; }
.question-card .row > input[type=text] { width: auto; flex: 1; min-width: 8rem; }
.question-card .row { flex-wrap: nowrap; }
.prose-editor { font-family: inherit; font-size: .98rem; line-height: 1.55; }

/* --- a school's own mark in the topbar (2026-09-15) ------------------------
   Org staff spend a whole term in this console; seeing their institution in
   the corner is how they know at a glance which account they are in. On
   white for the same reason the join page does it: a one-colour university
   mark is invisible on this bar in the dark scheme. */
.org-plate { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
             background: #fff; border: 1px solid var(--border); border-radius: var(--radius); padding: 4px 8px; }
.org-mark { display: block; max-height: 30px; max-width: 132px; width: auto; height: auto; }
/* A seal is square with fine ring text; at wordmark height it is a blob. */
.org-plate.square { padding: 3px; }
.org-plate.square .org-mark { max-height: 40px; max-width: 40px; }
/* No logo, or it failed to load: the school's initials in its colour. */
.org-plate.monogram { width: 40px; height: 40px; padding: 0; }
.org-monogram { font-weight: var(--fw-bold); font-size: .95rem; letter-spacing: .02em; color: var(--brand-text-light, var(--band)); }
.org-plate { border-color: transparent; }
@media (max-width: 640px) { .org-mark { max-width: 84px; } }

/* --- who needs help (2026-09-22) ------------------------------------------
   The one sentence a teacher reads in the doorway. Bigger than body text
   because it IS the answer; everything under it is the evidence for it. */
.lead { font-size: 1.4rem; font-weight: 700; line-height: 1.3; margin: .1rem 0 .4rem; letter-spacing: var(--tracking-tight); }

/* --- one course header (2026-10-16, audit C7) -----------------------------
   Timeline | Details on the Details view (courses.js faceSwitch) is the same toggle the faculty course page's
   bar has (workspace-theme.css: Shawn's "By lesson / By figure"): one frame, the buttons plain, the one in force
   on the accent's tint in the accent. It was a filled primary beside a quiet button. `span.` so that it
   outweighs timeline.css's .face-switch, which loads later. */
span.face-switch { gap: 0; border: 1px solid var(--border-strong); border-radius: 7px; overflow: hidden; }
span.face-switch .btn { border: 0; border-radius: 0; background: transparent; color: var(--text-dim); font-weight: var(--fw-regular); font-size: var(--fs-small); padding: .45rem .8rem; }   /* the size the course page's toggle has (L8) */
span.face-switch .btn:hover { background: var(--accent-soft); color: var(--text); filter: none; }
span.face-switch .btn[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); font-weight: var(--fw-bold); }
span.face-switch .btn:focus-visible { outline-offset: -3px; }   /* the frame cuts what is outside a button */

/* The flow chart's "to chase" pill navigates now, so it is a tab stop — and a
   tab stop nobody can see is not an improvement. Two rules on purpose: the
   outline is the ordinary focus ring, and the stroke is the belt-and-braces
   version for engines that still paint `outline` inconsistently on SVG. */
.chart [role="link"] { cursor: pointer; }
.chart [role="link"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.chart [role="link"]:focus-visible rect { stroke: var(--accent); stroke-width: 3; }

/* --- footer (2026-09-23) --------------------------------------------------
   Where the product name lives once the school leads the header. */
.foot { max-width: 1400px; margin: 0 auto; padding: .5rem 1.5rem 2rem; font-size: .8rem; color: var(--text-dim); }

/* --- a course's audio at a glance (2026-09-23) ----------------------------- */
.listen-row { display: flex; flex-wrap: wrap; gap: .3rem .35rem; align-items: center; }
.voice-note { font-size: .75rem; color: var(--warn); }
/* The preview player stays in view once something is loaded, so pressing
   play on lesson 3 of 16 does not start a voice from a player off-screen.
   Fixed to the window (2026-09-24), not sticky: sticky held it only while
   the Lessons card was on screen. courses.js sets data-docked, and
   body[data-player-docked] leaves room under the last card. Under dialogs
   (z 50) and toasts (60). */
.player-card[data-docked="1"] { position: fixed; z-index: 40; bottom: .75rem; left: 50%; transform: translateX(-50%);
                                width: min(980px, calc(100% - 1.5rem)); margin: 0;
                                box-shadow: 0 10px 32px rgba(16, 32, 42, .22);
                                border-color: color-mix(in srgb, var(--brand-fill) 35%, var(--border)); }
body[data-player-docked="1"] .panel { padding-bottom: 12rem; }
.player-card audio { display: none; }
.player-card[data-idle="1"] .pl-body { display: none; }

/* The player itself (2026-09-23): one big round play/pause in the school's
   colour, what is playing, and a track row. Icons are drawn in CSS, not ▶/❚❚
   glyphs, which some systems render as colour emoji. */
.pl-ico { display: inline-block; flex: 0 0 auto; }
.pl-ico-play { width: 0; height: 0; border-style: solid; border-color: transparent;
               border-width: .4em 0 .4em .66em; border-left-color: currentColor; margin-left: .12em; }
.pl-ico-pause { width: .62em; height: .78em; border-left: .22em solid currentColor;
                border-right: .22em solid currentColor; box-sizing: border-box; }

.play-toggle { display: inline-flex; align-items: center; gap: .4rem; }
.play-toggle .pl-ico { font-size: .8rem; }
.play-toggle.is-current { color: var(--brand-ink); background: var(--accent-soft); }
.play-toggle.is-playing { color: var(--brand-fill-ink); background: var(--brand-fill);
                          border-color: var(--brand-fill); }
.play-toggle.is-playing:hover { background: var(--brand-fill); filter: brightness(1.08); color: var(--brand-fill-ink); }

.pl-body { display: flex; align-items: center; gap: 1rem; margin: .25rem 0 .5rem; }
.pl-toggle { flex: 0 0 auto; width: 3.25rem; height: 3.25rem; border-radius: 50%; border: 0; cursor: pointer;
             display: inline-flex; align-items: center; justify-content: center; font-size: 1.25rem;
             background: var(--brand-fill); color: var(--brand-fill-ink);
             transition: transform .12s ease; }
.pl-toggle:hover { transform: scale(1.05); }
.pl-toggle:active { transform: scale(.96); }
.pl-toggle:focus-visible, .pl-skip:focus-visible, .pl-speed:focus-visible, .pl-seek:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; }
.pl-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: .4rem; }
.pl-now { display: flex; align-items: center; gap: .75rem; min-width: 0; }
.pl-names { min-width: 0; flex: 1 1 auto; }
.pl-title { font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl-sub { font-size: .82rem; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl-track { display: flex; align-items: center; gap: .5rem; }
.pl-time { font-size: .78rem; color: var(--text-dim); font-variant-numeric: tabular-nums; min-width: 2.4em; text-align: center; }
.pl-seek { flex: 1 1 auto; min-width: 4rem; accent-color: var(--brand-fill); cursor: pointer; }
.pl-skip, .pl-speed { flex: 0 0 auto; border: 1px solid var(--border); background: var(--surface);
                      color: var(--text); border-radius: var(--radius); padding: .15rem .55rem; font-size: .78rem;
                      font-weight: 600; cursor: pointer; font-variant-numeric: tabular-nums; }
.pl-skip:hover, .pl-speed:hover { background: var(--surface-2); }
.pl-speed { min-width: 3.1em; }

/* A small equaliser that moves only while something is playing. */
.pl-bars { display: inline-flex; align-items: flex-end; gap: 3px; height: 18px; flex: 0 0 auto; }
.pl-bars span { width: 3px; height: 30%; border-radius: 2px; background: var(--brand-fill); opacity: .45; }
.player-card[data-playing="1"] .pl-bars span { opacity: 1; animation: pl-eq 900ms ease-in-out infinite; }
.player-card[data-playing="1"] .pl-bars span:nth-child(2) { animation-delay: -300ms; }
.player-card[data-playing="1"] .pl-bars span:nth-child(3) { animation-delay: -600ms; }
.player-card[data-playing="1"] .pl-bars span:nth-child(4) { animation-delay: -150ms; }
@keyframes pl-eq { 0%, 100% { height: 25%; } 50% { height: 100%; } }
/* Compact when docked on a phone (2026-09-24): it covered ~38% of a 390px
   screen. One tight bar across the bottom edge: play, what is playing, the
   waveform/slider and the time. The heading, the note, the skips and the
   face go; speed stays (a teacher checks pace). */
@media (max-width: 700px) {
  .player-card[data-docked="1"] { bottom: 0; width: 100%; border-radius: var(--radius-lg) var(--radius-lg) 0 0;
                                  padding: .5rem .75rem calc(.5rem + env(safe-area-inset-bottom, 0px)); }
  .player-card[data-docked="1"] .card-head, .player-card[data-docked="1"] .pl-note,
  .player-card[data-docked="1"] .pl-skip, .player-card[data-docked="1"] .pl-face,
  .player-card[data-docked="1"] .pl-bars, .player-card[data-docked="1"] .pl-sub { display: none; }
  .player-card[data-docked="1"] .pl-body { margin: 0; gap: .6rem; }
  .player-card[data-docked="1"] .pl-toggle { width: 2.6rem; height: 2.6rem; font-size: 1rem; }
  .player-card[data-docked="1"] .pl-main { gap: .15rem; }
  .player-card[data-docked="1"] .pl-title { font-size: .88rem; }
  .player-card[data-docked="1"] .pl-track { gap: .35rem; }
  body[data-player-docked="1"] .panel { padding-bottom: 7rem; }
}
@media (prefers-reduced-motion: reduce) {
  .player-card[data-playing="1"] .pl-bars span { animation: none; height: 70%; }
  .pl-toggle { transition: none; }
}
.about dl { display: grid; grid-template-columns: max-content 1fr; gap: .35rem 1rem; margin: 0; }
.about dt { color: var(--text-dim); font-size: .85rem; }
.about dd { margin: 0; }

/* --- phone width (2026-09-23) --------------------------------------------
   A professor opens this between classes on a phone. Tables become a stack
   of small cards, each cell labelled from its column (ui.js sets data-label
   from our own column names; attr() renders it as text). */
@media (max-width: 700px) {
  body { font-size: 15px; }
  .topbar { --gutter: 1rem; gap: .5rem; }   /* its height is --topbar-h here too: the 56px this rule used to set is the band everywhere now */
  .topbar-left { gap: .6rem; }
  /* The right side is three fixed controls here (the name is hidden), so it
     must keep its width. Left able to shrink (min-width: 0), the avatar's
     block collapsed to 17px on a 390px phone and the avatar sat over the
     theme button. The school's name gives up the room instead; it already
     ends in an ellipsis. */
  .topbar-right { gap: .3rem; flex: none; }
  .who { display: none; }
  .scope-name { font-size: .98rem; }
  .stats { --gutter: 1rem; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; padding-top: 1rem; }
  .stat { min-width: 0; padding: .8rem .9rem; }
  .stat-value { font-size: 1.45rem; }
  /* One row that scrolls sideways (2026-09-24). Wrapping took three lines of
     a phone screen, and the bar does not stick on a phone: a sticky bar plus
     the docked player left little room for the page itself. The fade that
     says there is more is .tabs[data-more] above, at the edge that is cut;
     2.5rem of it here, where nothing scrolls under the row. */
  .tabs { --gutter: .5rem; --fade: 2.5rem; position: relative; flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
          scroll-padding-inline: 1rem; }
  .tab { --tab-px: .65rem; padding-top: .55rem; padding-bottom: .7rem; }
  .signout-label { display: none; }
  .topbar #signout { width: var(--band-ctl); padding-inline: 0; }   /* its icon alone, in the palette button's square */
  .modal-backdrop { padding: 0; align-items: flex-end; }
  .modal { border-radius: var(--radius-lg) var(--radius-lg) 0 0; max-height: 92vh; overflow-y: auto; }
  .modal-foot { position: sticky; bottom: 0; }
  .toasts, .toasts[data-over-modal] { left: 1rem; right: 1rem; }
  .toast { max-width: none; }
  .panel { padding: 1rem; }
  .demo-band { padding: 0 1rem; margin-top: .8rem; }
  .card { padding: 1rem; }
  .foot { padding: .5rem 1rem 1.5rem; }
  .lead { font-size: 1.2rem; }
  .card-head > .row { width: 100%; }
  .card-head > .row > select { flex: 1 1 auto; width: 100%; }

  .table-wrap { overflow-x: visible; }
  .table-wrap table, .table-wrap tbody, .table-wrap tr, .table-wrap td { display: block; width: 100%; }
  .table-wrap thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .table-wrap tr { border: 1px solid var(--border); border-radius: var(--radius); padding: .5rem .7rem; margin-bottom: .6rem; }
  .table-wrap td { border: 0; padding: .22rem 0; text-align: left; display: flex; gap: .75rem;
                   justify-content: space-between; align-items: baseline; }
  .table-wrap td.num { text-align: right; }
  .table-wrap td[data-label]::before { content: attr(data-label); flex: 0 0 auto; font-size: .75rem;
                   text-transform: uppercase; letter-spacing: .04em; color: var(--text-dim); font-weight: 700; }
  .table-wrap td > * { min-width: 0; }
  /* A chip in a card wraps inside it (re-review R4): an organization's name with its role ("… Other College · Owner")
     is wider than a phone's card, and a chip is one line everywhere else, so People scrolled sideways at 390. */
  .table-wrap td .chip, .table-wrap td .badge { white-space: normal; overflow-wrap: anywhere; max-width: 100%; }
  /* A cell holding a control (the teaching-style dropdown) wraps under its
     label instead of squeezing the control to "Standa…". */
  .table-wrap td:has(select), .table-wrap td:has(input) { flex-wrap: wrap; }
  .table-wrap td:has(select) > :not(style), .table-wrap td:has(input) > :not(style) { flex: 1 1 100%; }
  .table-wrap td select { width: 100%; }
  .table-wrap td:first-child { font-size: 1rem; }
  .table-wrap td:first-child::before { display: none; }
  .table-wrap td:first-child { display: block; }
  /* A leading "#" column is a quiet ordinal above the row, not a big lone number. */
  .table-wrap td.num:first-child { text-align: left; font-size: .78rem; color: var(--text-dim); padding-bottom: 0; }
  .player-card { bottom: .5rem; }
  .player-card[data-idle="0"] .pl-note { display: none; }
  .pl-body { gap: .7rem; }
  .pl-toggle { width: 2.9rem; height: 2.9rem; }
  .pl-track { flex-wrap: wrap; }
  .pl-seek { order: -1; flex-basis: 100%; }
  .pl-seek + .pl-time::before { content: '/ '; }
}

/* A student's signed-in devices (students.js devicesSection): the foot of the
   page, muted, separated by a hairline — an occasional act, not the subject. */
.devices { border-top: 1px solid var(--border); padding-top: .75rem; color: var(--text-dim); }
.devices h3 { font-size: .9rem; font-weight: 600; margin: 0; color: var(--text-dim); }
.devices-list { list-style: none; margin: .4rem 0 0; padding: 0; }
.devices-list li { padding: .15rem 0; overflow-wrap: anywhere; }
.devices-list li > span:first-child { color: var(--text); }

/* --- the correction ladder (corrections.js, 2026-09-23) ------------------
   One correction per row. The left rule carries the state the badge names,
   so a list of six can be scanned for "what is in use" without reading. */
.corrections { list-style: none; margin: .25rem 0 .5rem; padding: 0; display: flex; flex-direction: column; gap: .6rem; }
.correction { border: 1px solid var(--border); border-left: 4px solid var(--border-strong);
              border-radius: var(--radius); padding: .75rem .9rem; background: var(--surface); }
.correction:has(.badge-ok) { border-left-color: var(--ok); }
.correction:has(.badge-warn) { border-left-color: var(--warn); }
.correction:has(.badge-accent) { border-left-color: var(--accent); }
/* Pulled reads as set aside by its frame, not by fading the words: opacity
   .72 took the dim text under 4.5:1 in Light (2026-09-24). --text-dim is
   5.1:1 on --surface-2 in every theme. */
.correction.is-pulled { background: var(--surface-2); border-style: dashed; border-left-style: solid; }
.correction.is-pulled .correction-body { color: var(--text-dim); text-decoration: line-through; text-decoration-color: var(--border-strong); }
.correction-head { display: flex; justify-content: space-between; align-items: flex-start; gap: .5rem 1rem; flex-wrap: wrap; }
.correction-framing { font-size: 1.02rem; }
.correction-actions { gap: .35rem; }
.correction-blurb { margin-top: .15rem; font-style: italic; }
.correction-body { margin: .5rem 0 .2rem; line-height: 1.5; white-space: pre-wrap; max-width: 72ch; }
.correction-stats { margin-top: .35rem; color: var(--text); font-variant-numeric: tabular-nums; }
.note { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); padding: .5rem .7rem; }

/* How this student learns: one plain sentence, then the counts it rests on. */
.learner-sentence { font-size: 1.08rem; font-weight: 600; line-height: 1.45; margin: .1rem 0 .8rem; max-width: 70ch; }
.learner-card h4 { margin: 1rem 0 .35rem; font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; color: var(--text-dim); }
.habits { display: grid; grid-template-columns: minmax(8rem, max-content) 1fr; gap: .3rem 1rem; margin: 0 0 .6rem; }
.habits dt { color: var(--text-dim); }
.habits dd { margin: 0; }

/* Worked N of M, as a proportion bar. */
.meter { height: 8px; border-radius: 999px; background: var(--surface-2); overflow: hidden; min-width: 60px; margin-top: .45rem; }
.meter > span { display: block; height: 100%; background: var(--ok); }
tr.group-start > td { border-top: 1px solid var(--border-strong); }

@media (max-width: 700px) {
  .habits { grid-template-columns: 1fr; gap: .1rem; }
  .habits dd { margin-bottom: .4rem; }
  .correction-actions { width: 100%; }
}
.correction-who { font-weight: 700; margin-bottom: .35rem; }

/* Review flags on a premade correction (042): the terms a teacher should check
   before approving, named once in the note and marked where they occur. */
.review-flags { margin: .55rem 0 .1rem; padding: .55rem .75rem; border-radius: var(--radius);
                background: var(--warn-soft); border: 1px solid var(--warn); color: var(--text); max-width: 72ch; }
.review-flags .small { color: var(--text); margin-top: .2rem; }
mark.flagged { background: var(--warn-soft); color: var(--warn); font-weight: 700; padding: 0 .2em;
               border-radius: 3px; box-shadow: inset 0 -2px 0 var(--warn); }
.review-flags mark.flagged { background: var(--surface); }

/* ===== DESIGN FOUNDATION: helper components (2026-09-23) ==================
   The CSS half of ui.js's U.icon / U.skeleton / U.emptyState / U.avatar /
   U.chip / U.stat. Tokens only; no raw colours below except white initials. */

/* --- icons --------------------------------------------------------------- */
.icon { display: inline-block; flex: 0 0 auto; vertical-align: -.2em; overflow: visible; }
.btn .icon, .tab .icon, .chip .icon { vertical-align: middle; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: .4rem; }

/* --- skeletons ------------------------------------------------------------ */
.skel { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-2) 0; }
.skel-bar, .skel-dot, .skel-col-bar {
  display: block; border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--surface-2) 0%, var(--surface-3) 40%, var(--surface-2) 80%);
  background-size: 300% 100%;
  animation: skel-shimmer 1.4s var(--ease) infinite;
}
.skel-bar { height: .8rem; width: 100%; }
.skel-bar.skel-short { width: 38%; }
.skel-bar.skel-mid { width: 72%; }
.skel-bar.skel-title { height: 1rem; width: 46%; }
.skel-bar.skel-num { height: 1.6rem; width: 44%; }
.skel-dot { width: 40px; height: 40px; border-radius: 50%; flex: 0 0 auto; }
.skel-dot.skel-dot-sm { width: 28px; height: 28px; }
.skel-row { display: flex; align-items: center; gap: var(--space-3); }
.skel-col { display: flex; flex-direction: column; gap: var(--space-2); flex: 1 1 auto; }
.skel-card { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius-lg);
             padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); box-shadow: var(--shadow-sm); }
.skel-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--space-4); }
.skel-cards > .sr-only { position: absolute; }
.skel-thead, .skel-tr { display: grid; grid-template-columns: 28px 2fr 1fr 1fr; gap: var(--space-4); align-items: center; }
.skel-thead { grid-template-columns: 2fr 1fr 1fr; padding-bottom: var(--space-2); border-bottom: 1px solid var(--border); }
.skel-tr { padding: var(--space-2) 0; border-bottom: 1px solid var(--border); }
.skel-plot { display: flex; align-items: flex-end; gap: var(--space-3); height: 180px;
             border-bottom: 1px solid var(--border); padding: 0 var(--space-2); }
.skel-col-bar { flex: 1 1 0; border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.skel-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--space-3); }
.skel-stat { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius);
             padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); }
@keyframes skel-shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .skel-bar, .skel-dot, .skel-col-bar { animation: none; } }

/* --- empty states ---------------------------------------------------------
   A sentence and an action, set where the content would have started: left
   aligned, at reading width, with a small quiet mark in the margin
   (2026-10-11). It was a poster: an icon in a glowing disc over centred grey
   text, in the middle of an otherwise empty card. */
.empty-state { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: var(--space-3); row-gap: var(--space-1);
               justify-items: start; align-items: start; text-align: left;
               padding: var(--space-4) 0 var(--space-2); color: var(--text-dim); }
.empty-state.compact { padding: var(--space-2) 0; }
.empty-state > * { grid-column: 2; min-width: 0; }
.empty-icon { grid-column: 1; grid-row: 1; display: inline-flex; align-items: center; justify-content: center;
              height: calc(var(--fs-h3) * var(--lh));   /* centred on the title's first line */
              color: var(--text-dim); }
.empty-icon .icon { width: 18px; height: 18px; }
.empty-title { color: var(--text); font-weight: var(--fw-semibold); font-size: var(--fs-h3); line-height: var(--lh); }
.empty-body { max-width: 62ch; margin: 0; font-size: var(--fs-small); line-height: var(--lh); }
.empty-action { margin-top: var(--space-3); }
/* ONE CARD BORROWS THE MARK AND IS NOT AN EMPTY STATE: "Every lesson is approved", the end of a review
   (studio.js done(): div.card.rv-done, which editor.css centres). The rules above turned its 34px tick into
   the small grey mark of a page with nothing on it. There it is a tick in --ok at the size it is drawn,
   centred over the heading the card centres (2026-10-13). */
.rv-done .empty-icon { display: flex; justify-content: center; height: auto; color: var(--ok); }
.rv-done .empty-icon .icon { width: 34px; height: 34px; }

/* --- avatars -------------------------------------------------------------- */
.avatar { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
          border-radius: 50%; color: #fff; font-weight: var(--fw-semibold); letter-spacing: .01em;
          line-height: 1; user-select: none;
          box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14); }

/* --- a person in a list: avatar, name, email ------------------------------ */
.person { display: inline-flex; align-items: center; gap: .65rem; min-width: 0; max-width: 100%; vertical-align: middle; }
.person-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.person-name { font-weight: var(--fw-semibold); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.person-sub { font-size: var(--fs-caption); color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- the one-line "nothing here" (U.empty) --------------------------------- */
.empty { display: flex; align-items: flex-start; gap: var(--space-2); padding: var(--space-4) 0;
         text-align: left; color: var(--text-dim); font-size: var(--fs-small); line-height: var(--lh); }
.empty-inline-icon { color: var(--text-dim); display: inline-flex; margin-top: 1px; }

/* --- chips (and the older .badge, which now looks identical) ------------- */
.chip, .badge {
  display: inline-flex; align-items: center; gap: .3rem; vertical-align: middle;
  padding: .14rem .55rem; border-radius: var(--radius-pill);
  font-size: var(--fs-caption); font-weight: var(--fw-semibold); line-height: 1.35; letter-spacing: .01em;
  border: 1px solid transparent; white-space: nowrap;
}
.chip-ok, .badge-ok { background: var(--ok-soft); color: var(--ok); border-color: color-mix(in srgb, var(--ok) 22%, transparent); }
.chip-warn, .badge-warn { background: var(--warn-soft); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 24%, transparent); }
.chip-danger, .badge-danger { background: var(--danger-soft); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 22%, transparent); }
.chip-info { background: var(--info-soft); color: var(--info); border-color: color-mix(in srgb, var(--info) 22%, transparent); }
.chip-accent, .badge-accent { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 22%, transparent); }
.chip-brand { background: var(--brand-soft); color: var(--brand-ink); border-color: color-mix(in srgb, var(--brand-fill) 26%, transparent); }
.chip-quiet, .badge-quiet { background: var(--surface-2); color: var(--text-dim); border-color: var(--border); }

/* --- stat tiles ------------------------------------------------------------
   A label, a figure in tabular numerals, a line of context. No icon chip: the
   label already names the figure, and a tinted square on every tile was
   decoration (2026-10-11). The mark stays on a tile that is FLAGGED, where it
   is the second signal beside the warm colour.
   min-width is 0, not 150px: a tile is always in a grid, and the grid decides
   its width. At 150px four tiles in the course page's hero (tracks of ~113px
   at a 960px window) overran their tracks and sat on top of each other. */
.stat { position: relative; background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius);
        padding: var(--space-4) var(--space-5); min-width: 0;
        transition: border-color var(--dur) var(--ease); }
.stat-top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.stat-label { font-size: var(--fs-small); color: var(--text-dim); font-weight: var(--fw-medium); min-width: 0; }
.stat-icon { display: none; }
.stat-value { font-size: 1.75rem; font-weight: var(--fw-semibold); line-height: 1.15; letter-spacing: -.02em; margin-top: var(--space-1); color: var(--text); }
.stat-sub { font-size: var(--fs-caption); color: var(--text-dim); margin-top: 2px; font-variant-numeric: tabular-nums; }
.stat-label, .stat-value, .stat-sub { overflow-wrap: anywhere; }
.stat-trend { display: inline-flex; align-items: center; gap: .25rem; margin-top: var(--space-2);
              font-size: var(--fs-caption); font-weight: var(--fw-semibold); }
.stat-trend.trend-good { color: var(--ok); }
.stat-trend.trend-bad { color: var(--danger); }
.stat-trend.trend-neutral { color: var(--text-dim); }
.stat.clickable:hover { border-color: var(--border-strong); background: var(--surface-2); }
.stat.attention { border-color: color-mix(in srgb, var(--warn) 45%, var(--border)); background: var(--warn-soft); }
.stat.attention.clickable:hover { border-color: var(--warn); background: var(--warn-soft); }
.stat.attention .stat-value, .stat.attention .stat-label { color: var(--warn); }
.stat.attention .stat-icon { display: inline-flex; flex: 0 0 auto; color: var(--warn); }

/* The HEADER's attention tile ("1 course waiting for your review") is a call
   to act, not a figure to compare: one compact row with an arrow, two columns
   wide. Scoped to #stats so a tab's own row of tiles (Who needs help) keeps
   equal tiles and only gains the warm outline. */
#stats .stat.attention { display: flex; align-items: center; gap: .9rem; grid-column: span 2; padding: .75rem 1rem; }
#stats .stat.attention .stat-value { margin: 0; font-size: 1.5rem; order: 0; }
#stats .stat.attention .stat-top { order: 1; flex: 1 1 auto; }
#stats .stat.attention .stat-label { font-weight: var(--fw-semibold); font-size: var(--fs-body); }
#stats .stat.attention.clickable::after { content: "\2192"; order: 2; color: var(--warn); font-weight: var(--fw-bold); }
#stats .stat.attention.clickable .stat-icon { display: none; }   /* the arrow says "go"; a triangle beside it is noise */
@media (max-width: 700px) { #stats .stat.attention { grid-column: 1 / -1; } }

/* Keep the phone summary secondary to the course workspace. */
@media (max-width: 700px) {
  #stats { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .375rem .5rem; padding-top: .5rem; }
  #stats .stat:not(.attention) { display: flex; flex-wrap: wrap; align-items: center; align-content: center; justify-content: center;
    gap: 0 .35rem; min-height: 44px; padding: .25rem; border: 0; background: transparent; box-shadow: none; }
  #stats .stat:not(.attention) .stat-top { min-width: 0; }
  #stats .stat:not(.attention) .stat-value { order: -1; flex: 0 0 auto; max-width: 100%; margin: 0; font-size: 1rem; }
  #stats .stat:not(.attention) .stat-sub { flex-basis: 100%; margin: 0; text-align: center; font-size: .6875rem; line-height: 1.2; }
  #stats .stat.attention { min-height: 44px; padding: .5rem .75rem; gap: .5rem; }
  #stats .stat.attention .stat-value { flex: 0 0 auto; font-size: 1rem; }
  #stats .stat.attention .stat-label { font-size: var(--fs-small); font-weight: var(--fw-medium); }
}

/* FORCED COLOURS (Windows High Contrast) DROP BOX-SHADOWS, and the rules of a ruled readout are drawn with
   one: the figure tiles of .att-stats (cards.css), .house-stats (house.css) and .hero-stats (media.css) have
   `border: 0` and a 1px ring instead. There they get a real border (2026-10-13). `body` is in each selector
   because the owner's `border: 0` is as specific as `.att-stats .stat` and its stylesheet loads after this
   one: without it this rule would lose. */
@media (forced-colors: active) {
  body .att-stats .stat, body .house-stats .stat, body .hero-stats .stat { border: 1px solid CanvasText; }
}

/* Unused premade drafts, folded under the list (corrections.js earlierDrafts). */
.earlier-drafts { margin: .4rem 0 .6rem; color: var(--text-dim); }
.earlier-drafts > summary { cursor: pointer; font-weight: 600; padding: .3rem 0; }
.earlier-drafts .drafts { list-style: none; margin: .3rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.earlier-drafts .drafts > li { border-left: 3px solid var(--border); padding: .2rem 0 .2rem .7rem; }
.earlier-drafts .correction-body { color: var(--text-dim); }

/* Fields: a text input fills its field, like the textarea beneath it. Course
   settings' Title and "Where this comes from" were browser-default width. */
label.field > input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=number]) { width: 100%; }

/* The new-course document picker (studio.js uploadDialog). */
.dropzone { position: relative; display: flex; flex-direction: column; align-items: center; gap: .3rem;
            padding: 1.4rem 1rem; border: 2px dashed var(--border-strong); border-radius: var(--radius);
            background: var(--surface-2); color: var(--text-dim); text-align: center; cursor: pointer;
            transition: border-color .15s, background .15s; }
.dropzone:hover, .dropzone.is-over, .dropzone:focus-within { border-color: var(--accent); background: var(--accent-soft); color: var(--text); }
.dropzone.has-file { border-style: solid; border-color: var(--ok); }
.dropzone-lead { font-weight: 600; color: var(--text); }
.dropzone-name { font-size: var(--fs-small); overflow-wrap: anywhere; }
.dropzone-input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
/* A whole folder dropped or chosen (studio.js, only where builds from several files are on): the quiet button
   under the box, the files that will be used in reading order, and the ones that will not, each with its reason. */
.dropzone-folder { display: flex; align-items: center; flex-wrap: wrap; gap: .3rem .6rem; margin: -.4rem 0 1rem; }
.dropzone-notused, .dropzone-files { margin: 0 0 1rem; padding: .5rem .75rem; border: 1px solid var(--border);
                                     border-radius: var(--radius); background: var(--surface-2);
                                     font-size: var(--fs-small); color: var(--text-dim); }
.dropzone-notused { border-left: 3px solid var(--warn); }
.dropzone-notused-lead { margin: 0; font-weight: var(--fw-semibold); color: var(--text); }
.dropzone-notused ul, .dropzone-files ol { margin: .3rem 0 0; padding-left: 1.25rem; max-height: 9.5rem; overflow-y: auto;
                                           overflow-wrap: anywhere; }
.dropzone-notused p.small { margin: .4rem 0 0; }

/* A lesson a correction put right (attention.js putRightLine). */
.sc-putright { display: flex; gap: .4rem; align-items: center; color: var(--ok); margin-top: .35rem; }
.sc-putright span { color: var(--text); }


/* ===== Easy reading: the rules a token cannot carry ===================== */
[data-theme="reading"] body { letter-spacing: .012em; word-spacing: .08em; font-feature-settings: normal; }
[data-theme="reading"] a { text-decoration: underline; text-underline-offset: .2em; text-decoration-thickness: .08em; }
/* Emphasis by weight, never slant: italics are the hardest text to read. */
[data-theme="reading"] *, [data-theme="reading"] *::before, [data-theme="reading"] *::after {
  font-style: normal !important; animation: none !important; transition: none !important; }
[data-theme="reading"] em, [data-theme="reading"] i { font-weight: 700; }
/* Reading columns, not full-width lines. */
[data-theme="reading"] p, [data-theme="reading"] .empty-body,
[data-theme="reading"] .learner-sentence, [data-theme="reading"] .correction-body { max-width: 70ch; }
/* Generous targets. */
[data-theme="reading"] .btn { min-height: 44px; padding-inline: 1rem; }
[data-theme="reading"] .btn-sm { min-height: 40px; }
[data-theme="reading"] .topbar { --band-ctl: 40px; }   /* the band's two buttons are small buttons: both keep this theme's 40px */
[data-theme="reading"] .tab { min-height: 48px; font-size: var(--fs-body); }
[data-theme="reading"] input, [data-theme="reading"] select { min-height: 44px; }
/* (A table's headings keep the console's caption here, 15px, as every other text does: a rule set them to 14px, under
   the floor of this theme; re-review R5.) And they are written as words, in their own case, at their own spacing, and
   wrap: in capitals widened onto one line they squeezed a student's course column to nothing (2026-10-16, QA T12). */
[data-theme="reading"] th { text-transform: none; letter-spacing: normal; white-space: normal; }
[data-theme="reading"] .skel-bar, [data-theme="reading"] .skel-dot { background: var(--surface-3); }
[data-theme="reading"] :focus-visible { outline-width: 3px; }

/* ===== the theme picker (ui.js U.themePicker / U.themeButton) =========== */
.theme-anchor { position: relative; }
.theme-pop { position: absolute; right: 0; top: calc(100% + 8px); z-index: 70; width: 290px;
             background: var(--surface-1); color: var(--text); border: 1px solid var(--border);
             border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); padding: .9rem;
             animation: rise var(--dur) var(--ease-out) both; }
.theme-pop h2 { font-size: var(--fs-small); margin: 0 0 .5rem; color: var(--text-dim);
                text-transform: uppercase; letter-spacing: var(--tracking-caps); }
.theme-list { display: flex; flex-direction: column; gap: .25rem; border: 0; margin: 0; padding: 0; min-width: 0; }
.theme-opt { display: flex; align-items: center; gap: .7rem; padding: .5rem .55rem; border-radius: var(--radius);
             cursor: pointer; border: 1px solid transparent; }
.theme-opt:hover { background: var(--surface-2); }
.theme-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.theme-opt:has(input:checked) { border-color: var(--brand-fill); background: var(--brand-soft); }
/* :focus, not only :focus-visible: the menu moves focus to the chosen theme
   when it opens, and a click on the palette button would otherwise leave no
   ring to say where the keyboard now is (2026-09-24). */
.theme-opt:has(input:focus), .theme-opt:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 1px; }
.theme-name { font-weight: var(--fw-semibold); display: block; line-height: 1.25; }
.theme-note { font-size: var(--fs-caption); color: var(--text-dim); display: block; line-height: 1.3; }
.theme-check { margin-left: auto; color: var(--brand-ink); visibility: hidden; }
.theme-opt:has(input:checked) .theme-check { visibility: visible; }
/* A swatch paints the theme it names, whatever theme is in force. */
.theme-swatch { flex: 0 0 auto; width: 34px; height: 26px; border-radius: 7px; position: relative; overflow: hidden;
                border: 1px solid rgba(0, 0, 0, .18); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06); }
.theme-swatch::before { content: ""; position: absolute; left: 5px; right: 5px; top: 6px; height: 4px; border-radius: 2px; background: var(--sw-ink); }
.theme-swatch::after { content: ""; position: absolute; left: 5px; width: 12px; bottom: 5px; height: 6px; border-radius: 3px; background: var(--sw-accent); }
.theme-swatch[data-swatch="blue"] { background: #0e1928; --sw-ink: #eaf3fc; --sw-accent: #78d1ff; }
.theme-swatch[data-swatch="light"] { background: #ffffff; --sw-ink: #0e1928; --sw-accent: #0b6896; }
.theme-swatch[data-swatch="dark"] { background: #0e1928; --sw-ink: #eaf3fc; --sw-accent: #78d1ff; }   /* Dark is LORE Blue: not offered, kept for a stored choice */
.theme-swatch[data-swatch="reading"] { background: #FBF7EE; --sw-ink: #111111; --sw-accent: #0B4A8B; }
.theme-swatch[data-swatch="hologram"] { background: #0A1422; --sw-ink: #EBF6FF; --sw-accent: #6BD2FF; }
.theme-swatch[data-swatch="scholastic"] { background: #FFFDF6; --sw-ink: #22252B; --sw-accent: #1F3A5F; }
.theme-swatch[data-swatch="dusk"] { background: #24272C; --sw-ink: #E4E1D9; --sw-accent: #9CC2A9; }
.theme-swatch[data-swatch="system"] { background: linear-gradient(135deg, #ffffff 0 50%, #0e1928 50% 100%); --sw-ink: #7d8a96; --sw-accent: #0b6896; }
/* The sign-in page's compact row. */
.signin-theme { position: static; transform: none; width: min(560px, 100%); }
.signin-theme .theme-list { flex-direction: row; flex-wrap: wrap; justify-content: center; gap: .35rem; }
.signin-theme .theme-opt { padding: .35rem .6rem .35rem .4rem; gap: .45rem; background: color-mix(in srgb, var(--surface-1) 70%, transparent); border-color: var(--border); }
.signin-theme .theme-note, .signin-theme .theme-check { display: none; }
.signin-theme .theme-swatch { width: 22px; height: 18px; border-radius: 5px; }
.signin-theme .theme-swatch::before { left: 3px; right: 3px; top: 4px; height: 3px; }
.signin-theme .theme-swatch::after { left: 3px; width: 8px; bottom: 3px; height: 4px; }
.signin-theme .theme-name { font-size: var(--fs-caption); font-weight: var(--fw-medium); }
.signin-theme-label { display: block; text-align: center; font-size: var(--fs-caption); color: var(--text-dim); margin-bottom: .4rem; }
@media (max-width: 700px) {
  .theme-pop { position: fixed; left: .75rem; right: .75rem; top: 64px; width: auto; }
}
/* Bigger type needs a wider label column in the class's lesson list (cards.css .lb). */
@media (min-width: 641px) {
  [data-theme="reading"] .lb { grid-template-columns: minmax(18rem, 40%) minmax(0, 1fr) auto; }
}
.lb-took { white-space: nowrap; }
/* A heading given focus by code (so a screen reader starts there) is not a
   control; a ring round it reads as a selected box. */
[tabindex="-1"]:focus, [tabindex="-1"]:focus-visible { outline: none; }
