/* ============================================================
   TYPEFACE — the system stack, everywhere, by the owner's explicit
   decision (2026-07-16): the site ships NO fonts. Every device renders
   its native face (Segoe UI / San Francisco / Roboto), body and wordmark
   alike — the wordmark's identity is carried by the .logo span gradient,
   not by a typeface. Zero font bytes on the wire; KaTeX's vendored math
   fonts are the deliberate exception. If a shipped face is ever
   reconsidered, self-host it (DPDP — Instructions/LEGAL.md) and mind that
   font-src is 'self'.
   ============================================================ */

* { margin: 0; padding: 0; box-sizing: border-box; }

/* ============================================================
   DESIGN TOKENS
   Two independent axes:
     · BRAND  — which palette (meadow, indigo, …). Set by the admin.
     · MODE   — light / dark. Chosen by the visitor.
   Everything below is the meadow brand. :root holds meadow-light so the
   site still renders correctly if theme JS never runs.

   Three groups, and the distinction matters when adding a theme:
     · Brand tokens   — a new theme MUST redefine these.
     · Semantic tokens — success/danger/live/gold. Meaning is tied to the
       colour (red = danger), so they stay put across brands. A theme should
       only touch them if the brand actively clashes.
     · Artwork colours — NOT tokens. The .scene block near the bottom of this
       file is hand-matched to the meadow PNGs; it is scoped to the meadow
       brand rather than themed. See THEMES.md.
   ============================================================ */

:root {
  /* ---- Brand: meadow, light ---- */
  --bg-primary: #f7faf7;   /* the faintest green-white */
  --bg-secondary: #ffffff;
  --text-primary: #2b5c39;   /* meadow ink */
  --text-secondary: #587a5c;
  --accent-1: #2f6b3f;   /* forest green */
  --accent-2: #4f9c5b;   /* medium leaf green (inner pages) */
  --accent-solid: #3e7d4e;         /* filled primary button */
  --accent-solid-border: #2f5237;
  --text-on-accent: #ffffff;       /* text sitting on an accent fill */
  --outline-hover-ink: #ffffff;    /* .btn-outline text once filled on hover */
  --card-bg: rgba(255, 255, 255, 0.72);
  --card-border: rgba(255, 255, 255, 0.85);
  --nav-bg: rgba(255, 255, 255, 0.72);
  --shadow: rgba(31, 58, 36, 0.18);
  --input-bg: rgba(255, 255, 255, 0.8);
  --scrim: rgba(10, 20, 14, 0.55);          /* modal backdrops */
  --scrim-strong: rgba(10, 20, 14, 0.68);
  --icon-ring: #4c7c54;            /* .feature-icon field-journal badge */
  --icon-tint: #eaf3e4;
  --icon-ink: #2f6b3f;
  --auth-glow-1: rgba(79, 156, 91, 0.16);   /* login page radial wash */
  --auth-glow-2: rgba(47, 107, 63, 0.14);
  /* Night-only "lantern" primary button on the homepage — a meadow
     signature (see [data-mode="dark"] .home .btn-primary). */
  --lantern-1: #e8b54a;
  --lantern-2: #d9973a;
  --lantern-border: #8a5f1d;
  --lantern-ink: #241a05;
  --lantern-glow: rgba(255, 196, 90, 0.55);
  --lantern-glow-hover: rgba(255, 206, 110, 0.75);
  /* Inline page editor (.ltm-ed-*). Its bar is a fixed dark chrome that does
     NOT follow light/dark mode, so these are deliberately mode-independent. */
  --editor-bar-bg: #12241a;
  --editor-bar-ink: #eef4ec;
  --editor-accent: #4f9c5b;
  --editor-accent-hover: #5cab68;
  --editor-ring: rgba(79, 156, 91, 0.6);
  --editor-ring-soft: rgba(79, 156, 91, 0.25);
  --transition: all var(--dur-base) var(--ease-standard);

  /* ---- Semantic: shared by every brand ---- */
  --success: #10b981;
  --success-ink: #059669;
  --success-tint: rgba(16, 185, 129, 0.12);
  --success-msg: #00b894;          /* contact-form confirmation text */
  --danger: #dc3545;
  --danger-ink: #d32f2f;
  --danger-tint: rgba(244, 67, 54, 0.08);
  --danger-border: rgba(244, 67, 54, 0.25);
  --live: #ff5b5b;                 /* live-class pulse dot */
  --info-tint: rgba(99, 102, 241, 0.12);    /* search result kind pill */
  --info-mark: rgba(99, 102, 241, 0.18);    /* search query highlight */
  /* Announcement tones. Semantic, not brand: a warning is amber in every
     theme, exactly as --danger is red in every theme. Each tone supplies a
     `solid` (the rail, the icon, the button fill) and an `ink` (its text);
     the tint and edge are mixed from the solid, so a tone is two values.
     The `custom` tone has no entry here — .ann--custom takes its solid from
     the admin's hex inline, and everything else derives the same way. */
  --ann-info-solid: #2563eb;      --ann-info-ink: #1d4ed8;
  --ann-warning-solid: #d97706;   --ann-warning-ink: #92400e;
  --ann-danger-solid: #dc2626;    --ann-danger-ink: #b91c1c;
  --ann-success-solid: #10b981;   --ann-success-ink: #047857;
  --ann-neutral-solid: #64748b;   --ann-neutral-ink: #334155;
  --gold-1: #f5c518;               /* gold: access badge/CTA + marked-question accent */
  --gold-2: #e6a817;
  --gold-ink: #4a3403;
  --gold-glow: rgba(230, 168, 23, 0.35);
  --gold-glow-hover: rgba(230, 168, 23, 0.45);
  /* Legacy aliases — some pages (profile/admin/verify/dashboard) were
     written against these older names; keep them pointed at the canonical
     tokens so nothing silently renders unstyled. */
  --primary-color: var(--accent-1);
  --border-color: var(--card-border);
  --bg-card: var(--card-bg);
}

/* ---- Brand: meadow, dark ----
   Only the tokens that actually differ. --accent-solid stays the daytime
   moss on purpose: at night only the HOMEPAGE primary turns into the gold
   lantern, every other primary button keeps the moss fill. */
[data-mode="dark"] {
  /* Announcement inks only — the solids stay put. A notice's tint is a wash
     of its solid over a dark page, so the text on it has to come UP, not
     down; the light-mode inks would be unreadable there. This block is in
     the meadow-dark rule rather than a brand one because the tones are
     brand-independent, and every other brand's dark block inherits it. */
  --ann-info-ink: #93c5fd;
  --ann-warning-ink: #fcd34d;
  --ann-danger-ink: #fca5a5;
  --ann-success-ink: #6ee7b7;
  --ann-neutral-ink: #cbd5e1;
  --bg-primary: #0c140e;
  --bg-secondary: #122016;
  --text-primary: #eef4ea;
  --text-secondary: #b9c8b4;
  --accent-1: #6fae7c;   /* moonlit moss */
  --accent-2: #8cc79a;   /* moonlit leaf green (inner pages) */
  --outline-hover-ink: #10200f;
  --card-bg: rgba(8, 18, 12, 0.62);
  --card-border: rgba(255, 255, 255, 0.16);
  --nav-bg: rgba(8, 16, 11, 0.75);
  --shadow: rgba(0, 0, 0, 0.45);
  --input-bg: rgba(255, 255, 255, 0.08);
  --icon-ring: #7da268;
  --icon-tint: #1c2b1a;
  --icon-ink: #cfe3c2;
}

/* All text/accents stay green everywhere — the sun lives only in the
   artwork, never in the typography. */

/* ============================================================
   BRAND: INDIGO — the original LearnToMath palette, restored from
   commit 71710a8 (violet #6c5ce7 + teal #00cec9).

   Note the specificity ladder that makes this safe:
     [data-mode="dark"]                      (0,1,0)  meadow dark
     [data-theme="indigo"][data-mode="dark"] (0,2,0)  indigo dark — wins
   So a page that somehow ships without a data-theme still gets a working
   dark mode (meadow's) rather than an unstyled one.

   Indigo redefines the --lantern-* tokens rather than trying to escape the
   homepage night-button rule: at night the home CTA becomes an indigo
   gradient with an indigo glow, which is the same idea in its own palette.
   ============================================================ */
[data-theme="indigo"] {
  --bg-primary: #f7f8fc;
  --bg-secondary: #ffffff;
  --text-primary: #1a1a2e;
  --text-secondary: #5a5a72;
  --accent-1: #6c5ce7;   /* violet */
  --accent-2: #00cec9;   /* teal */
  --accent-solid: #6c5ce7;
  --accent-solid-border: #5b4bd6;
  --text-on-accent: #ffffff;
  --outline-hover-ink: #ffffff;
  --card-bg: rgba(255, 255, 255, 0.55);
  --card-border: rgba(255, 255, 255, 0.6);
  --nav-bg: rgba(255, 255, 255, 0.7);
  --shadow: rgba(108, 92, 231, 0.15);
  --input-bg: rgba(255, 255, 255, 0.8);
  --scrim: rgba(16, 16, 30, 0.55);
  --scrim-strong: rgba(16, 16, 30, 0.68);
  --icon-ring: #6c5ce7;
  --icon-tint: #eeecfd;
  --icon-ink: #4c3fd0;
  --auth-glow-1: rgba(108, 92, 231, 0.16);
  --auth-glow-2: rgba(0, 206, 201, 0.14);
  --lantern-1: #6c5ce7;
  --lantern-2: #00cec9;
  --lantern-border: transparent;
  --lantern-ink: #ffffff;
  --lantern-glow: rgba(108, 92, 231, 0.45);
  --lantern-glow-hover: rgba(108, 92, 231, 0.65);
  --editor-bar-bg: #16162a;
  --editor-bar-ink: #f1f1f6;
  --editor-accent: #6c5ce7;
  --editor-accent-hover: #7d6ef0;
  --editor-ring: rgba(108, 92, 231, 0.6);
  --editor-ring-soft: rgba(108, 92, 231, 0.25);
}

[data-theme="indigo"][data-mode="dark"] {
  --bg-primary: #0f0f1a;
  --bg-secondary: #16162a;
  --text-primary: #f1f1f6;
  --text-secondary: #a8a8c0;
  --accent-1: #8c7cff;
  --accent-2: #00e6d8;
  --accent-solid: #8c7cff;
  --accent-solid-border: #6c5ce7;
  /* Dark ink, not white: white on the moonlit violet/teal fills is 3.26:1,
     under WCAG AA's 4.5. Dark-on-light-accent is the standard dark-mode
     resolution (5.5:1 on violet, ~11:1 on teal). */
  --text-on-accent: #10102a;
  --outline-hover-ink: #0f0f1a;
  --card-bg: rgba(255, 255, 255, 0.06);
  --card-border: rgba(255, 255, 255, 0.12);
  --nav-bg: rgba(15, 15, 26, 0.75);
  --shadow: rgba(140, 124, 255, 0.2);
  --input-bg: rgba(255, 255, 255, 0.08);
  --icon-ring: #8c7cff;
  --icon-tint: #1e1c3a;
  --icon-ink: #c9c2ff;
  --lantern-1: #8c7cff;
  --lantern-2: #00e6d8;
  --lantern-ink: #0f0f1a;
  --lantern-glow: rgba(140, 124, 255, 0.5);
  --lantern-glow-hover: rgba(140, 124, 255, 0.7);
}

/* Indigo's structural signature: the primary button was a gradient with no
   border, where meadow's is a solid moss fill with one.

   background-origin MUST be repeated here, after the `background` shorthand:
   the shorthand resets every background-* longhand, so the copy on .btn gets
   wiped. Without it the gradient is sized to the padding box but painted out
   to the border box, and background-repeat (default: repeat) tiles it — so the
   2px border strip shows the gradient STARTING OVER, i.e. a violet crescent
   down the teal right-hand edge. Same reason it must be repeated in the
   night-lantern rule below. */
[data-theme="indigo"] .btn-primary,
[data-theme="azure"] .btn-primary {
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  background-origin: border-box;
  border-color: transparent;
  box-shadow: 0 8px 24px var(--shadow);
}
[data-theme="indigo"] .btn-primary:hover,
[data-theme="azure"] .btn-primary:hover { box-shadow: 0 12px 32px var(--shadow); }

/* ============================================================
   BRAND: AUTUMN — burnt orange + amber on warm parchment.

   Structurally this follows MEADOW, not indigo: a solid primary button
   with a border (no gradient), so it needs no .btn-primary override.

   It does redefine --lantern-*, because those live in :root as meadow's
   gold; without an override, autumn's homepage night button would inherit
   meadow's lantern. Autumn's is a deeper ember.
   ============================================================ */
[data-theme="autumn"] {
  --bg-primary: #fdf6ee;           /* warm parchment */
  --bg-secondary: #fffaf4;
  --text-primary: #46281a;         /* deep bark */
  --text-secondary: #8a6144;
  --accent-1: #b5451b;             /* burnt orange */
  --accent-2: #d98324;             /* amber */
  --accent-solid: #c0521f;
  --accent-solid-border: #8f380f;
  --text-on-accent: #ffffff;
  --outline-hover-ink: #ffffff;
  --card-bg: rgba(255, 250, 242, 0.74);
  --card-border: rgba(255, 255, 255, 0.85);
  --nav-bg: rgba(255, 248, 238, 0.75);
  --shadow: rgba(94, 48, 20, 0.18);
  --input-bg: rgba(255, 255, 255, 0.8);
  --scrim: rgba(38, 20, 10, 0.55);
  --scrim-strong: rgba(38, 20, 10, 0.68);
  --icon-ring: #c47a3d;
  --icon-tint: #fbeada;
  --icon-ink: #a8451a;
  --auth-glow-1: rgba(181, 69, 27, 0.16);
  --auth-glow-2: rgba(217, 131, 36, 0.16);
  --lantern-1: #e8913f;
  --lantern-2: #d2662a;
  --lantern-border: #8a4413;
  --lantern-ink: #2a1608;
  --lantern-glow: rgba(240, 150, 70, 0.5);
  --lantern-glow-hover: rgba(250, 170, 90, 0.7);
  --editor-bar-bg: #2a1a0f;
  --editor-bar-ink: #f6ece1;
  --editor-accent: #c0521f;
  --editor-accent-hover: #d4632c;
  --editor-ring: rgba(181, 69, 27, 0.6);
  --editor-ring-soft: rgba(181, 69, 27, 0.25);
}

/* (0,2,0) — beats the generic [data-mode="dark"] meadow block, as indigo does. */
[data-theme="autumn"][data-mode="dark"] {
  --bg-primary: #1a1008;           /* embers after dusk */
  --bg-secondary: #241710;
  --text-primary: #f6ece1;
  --text-secondary: #cbb29c;
  --accent-1: #e08a4c;
  --accent-2: #f0ad63;
  --accent-solid: #c0521f;         /* the fill stays daytime rust, as meadow's stays moss */
  --accent-solid-border: #8f380f;
  --outline-hover-ink: #1a1008;
  --card-bg: rgba(30, 18, 9, 0.62);
  --card-border: rgba(255, 255, 255, 0.14);
  --nav-bg: rgba(22, 13, 7, 0.78);
  --shadow: rgba(0, 0, 0, 0.45);
  --input-bg: rgba(255, 255, 255, 0.08);
  --icon-ring: #c07a45;
  --icon-tint: #2e1d10;
  --icon-ink: #f0cba5;
}

[data-theme="autumn"] .logo img { content: url(images/Logos/logo-autumn.png); }

/* ============================================================
   BRAND: AZURE — indigo's palette with the violet end swapped for blue
   (#2d6ae0 + the same teal #00cec9). Owner-picked 2026-08 from the note
   artwork, where the section pill runs blue -> teal rather than violet ->
   teal. Indigo is kept as its own theme, unchanged.

   Structurally azure IS indigo: it shares the gradient primary button and
   the sparkle hero motif (both selectors below list the two brands), so
   the only thing that differs is the colour.
   ============================================================ */
[data-theme="azure"] {
  --bg-primary: #f7f8fc;
  --bg-secondary: #ffffff;
  --text-primary: #15203a;
  --text-secondary: #566178;
  --accent-1: #2d6ae0;   /* blue */
  --accent-2: #00cec9;   /* teal — the same one indigo uses */
  --accent-solid: #2d6ae0;
  --accent-solid-border: #2559c4;
  --text-on-accent: #ffffff;
  --outline-hover-ink: #ffffff;
  --card-bg: rgba(255, 255, 255, 0.55);
  --card-border: rgba(255, 255, 255, 0.6);
  --nav-bg: rgba(255, 255, 255, 0.7);
  --shadow: rgba(45, 106, 224, 0.15);
  --input-bg: rgba(255, 255, 255, 0.8);
  --scrim: rgba(13, 20, 36, 0.55);
  --scrim-strong: rgba(13, 20, 36, 0.68);
  --icon-ring: #2d6ae0;
  --icon-tint: #e7eefc;
  --icon-ink: #1e56c0;
  --auth-glow-1: rgba(45, 106, 224, 0.16);
  --auth-glow-2: rgba(0, 206, 201, 0.14);
  --lantern-1: #2d6ae0;
  --lantern-2: #00cec9;
  --lantern-border: transparent;
  --lantern-ink: #ffffff;
  --lantern-glow: rgba(45, 106, 224, 0.45);
  --lantern-glow-hover: rgba(45, 106, 224, 0.65);
  --editor-bar-bg: #14192b;
  --editor-bar-ink: #f1f3f8;
  --editor-accent: #2d6ae0;
  --editor-accent-hover: #4a80ea;
  --editor-ring: rgba(45, 106, 224, 0.6);
  --editor-ring-soft: rgba(45, 106, 224, 0.25);
}

[data-theme="azure"][data-mode="dark"] {
  --bg-primary: #0e1220;
  --bg-secondary: #151b2c;
  --text-primary: #f1f3f8;
  --text-secondary: #a4aec6;
  --accent-1: #6f9bff;
  --accent-2: #00e6d8;
  --accent-solid: #6f9bff;
  --accent-solid-border: #2d6ae0;
  /* Dark ink for the same reason indigo uses it: white on the moonlit blue
     reads 2.69, under WCAG AA. Dark-on-light-accent gives 6.80. */
  --text-on-accent: #0d1428;
  --outline-hover-ink: #0e1220;
  --card-bg: rgba(255, 255, 255, 0.06);
  --card-border: rgba(255, 255, 255, 0.12);
  --nav-bg: rgba(14, 18, 32, 0.75);
  --shadow: rgba(111, 155, 255, 0.2);
  --input-bg: rgba(255, 255, 255, 0.08);
  --icon-ring: #6f9bff;
  --icon-tint: #16233f;
  --icon-ink: #b9d0ff;
  --lantern-1: #6f9bff;
  --lantern-2: #00e6d8;
  --lantern-ink: #0e1220;
  --lantern-glow: rgba(111, 155, 255, 0.5);
  --lantern-glow-hover: rgba(111, 155, 255, 0.7);
}

/* Placeholder: azure borrows indigo's violet book until its own blue mark
   is drawn. Swap this one URL for logo-azure.png when the file exists. */
[data-theme="azure"] .logo img { content: url(images/Logos/logo-indigo.png); }

/* ============================================================
   BRAND: MEADOW LIFTED — meadow's green with its range corrected.
   Owner-picked 2026-09-04 from a meadow-landscape reference, out of four
   candidates. Meadow's hue was right; its RANGE was not — the dark end sat
   too near black-green and the light end never got light, so the wordmark
   and every 135deg accent gradient read as one flat colour. Here the dark
   end lifts off black (#2f6b3f -> #357a42) and the light end goes genuinely
   light (#4f9c5b -> #7fc35f), so the gradient actually reads as a gradient.

   This is a brand of its own, NOT a recolour of meadow: meadow ships
   unchanged beside it.

   Structurally it follows MEADOW, not indigo: --accent-solid stays the
   daytime green with white ink in BOTH modes, so it needs no .btn-primary
   gradient rule and no dark-ink --text-on-accent. It inherits the leaf hero
   motif for the same reason.

   Surfaces are the white ones meadow moved to in the same change — this
   brand never had cream.
   ============================================================ */
[data-theme="meadow-lifted"] {
  --bg-primary: #f7faf7;           /* the faintest green-white */
  --bg-secondary: #ffffff;
  --text-primary: #2b5c39;
  --text-secondary: #587a5c;
  --accent-1: #357a42;             /* lifted forest green — 4.98 on the page */
  --accent-2: #7fc35f;             /* genuinely light leaf (not contrast-bearing) */
  --accent-solid: #357a42;
  --accent-solid-border: #275c31;
  --text-on-accent: #ffffff;       /* 5.23 on the fill, in both modes */
  --outline-hover-ink: #ffffff;
  --card-bg: rgba(255, 255, 255, 0.72);
  --card-border: rgba(255, 255, 255, 0.85);
  --nav-bg: rgba(255, 255, 255, 0.72);
  --shadow: rgba(31, 58, 36, 0.18);
  --input-bg: rgba(255, 255, 255, 0.8);
  --scrim: rgba(10, 20, 14, 0.55);
  --scrim-strong: rgba(10, 20, 14, 0.68);
  --icon-ring: #56925b;
  --icon-tint: #eaf5e2;
  --icon-ink: #357a42;
  --auth-glow-1: rgba(127, 195, 95, 0.16);
  --auth-glow-2: rgba(53, 122, 66, 0.14);
  /* Restated, not inherited: the night-button rule is brand-agnostic, so a
     brand that skips these silently picks up :root's gold. Meadow Lifted
     WANTS meadow's gold lantern — it is the same signature — but it has to
     say so, or the test that guards this can't tell the two cases apart. */
  --lantern-1: #e8b54a;
  --lantern-2: #d9973a;
  --lantern-border: #8a5f1d;
  --lantern-ink: #241a05;
  --lantern-glow: rgba(255, 196, 90, 0.55);
  --lantern-glow-hover: rgba(255, 206, 110, 0.75);
  --editor-bar-bg: #142a1b;
  --editor-bar-ink: #eef4ec;
  --editor-accent: #58a95f;
  --editor-accent-hover: #68b86e;
  --editor-ring: rgba(88, 169, 95, 0.6);
  --editor-ring-soft: rgba(88, 169, 95, 0.25);
}

/* (0,2,0) — beats the generic [data-mode="dark"] meadow block, as the other
   brands do. --accent-solid is deliberately absent: the fill stays the
   daytime green, exactly as meadow's stays moss and autumn's stays rust. */
[data-theme="meadow-lifted"][data-mode="dark"] {
  --bg-primary: #0c140e;
  --bg-secondary: #122016;
  --text-primary: #eef4ea;
  --text-secondary: #b9c8b4;
  --accent-1: #74b982;             /* 8.0 on the dark ground */
  --accent-2: #a6da75;             /* 11.5 */
  --outline-hover-ink: #10200f;
  --card-bg: rgba(8, 18, 12, 0.62);
  --card-border: rgba(255, 255, 255, 0.16);
  --nav-bg: rgba(8, 16, 11, 0.75);
  --shadow: rgba(0, 0, 0, 0.45);
  --input-bg: rgba(255, 255, 255, 0.08);
  --icon-ring: #7db569;
  --icon-tint: #1c2b1a;
  --icon-ink: #cfe3c2;
}

/* No .logo img rule on purpose: with none, the mark falls through to the
   <img src> in the HTML, which is meadow's green book — near enough to be a
   sane placeholder rather than a wrong colour. Add one pointing at
   logo-meadow-lifted.png when that file exists. */

/* ============================================================
   STRUCTURE TOKENS — brand-agnostic, deliberately separate from the
   colour tokens above (a brand recolours the site; it never re-spaces it).

   These are the TARGET vocabulary, not a description of every value in
   this file: the shipped CSS grew five different "small text" sizes and
   radii from 3px to 50px. Nothing has been migrated onto these wholesale —
   that churn isn't worth the regression risk in one sweep. The contract:
   NEW rules use tokens; existing rules migrate when a change already
   touches them, verified against the screenshot harness. Don't add new
   ad-hoc px values for these properties.
   ============================================================ */
:root {
  /* type scale */
  --text-xs: 12px;      /* fine print, table meta */
  --text-sm: 13px;      /* secondary UI text */
  --text-base: 14px;    /* default UI/body text */
  --text-md: 16px;      /* emphasized body, inputs */
  --text-lg: 18px;      /* card titles */
  --text-xl: 20px;      /* section subheads */
  --text-2xl: 24px;     /* section heads */
  --text-3xl: 28px;     /* page heads */
  --text-4xl: 34px;     /* hero display */
  --weight-medium: 600;
  --weight-bold: 700;
  --weight-black: 800;

  /* spacing ladder (padding, margin, gap) */
  --space-2: 2px;  --space-4: 4px;   --space-6: 6px;   --space-8: 8px;
  --space-12: 12px; --space-16: 16px; --space-20: 20px; --space-24: 24px;
  --space-32: 32px; --space-40: 40px; --space-48: 48px;

  /* corner radii */
  --radius-sm: 8px;     /* chips, small controls */
  --radius-md: 12px;    /* inputs, buttons */
  --radius-lg: 20px;    /* cards */
  --radius-xl: 24px;    /* hero cards, modals */
  --radius-full: 50px;  /* pills */

  /* elevation — composed on the per-brand --shadow colour */
  --elev-1: 0 4px 12px var(--shadow);
  --elev-2: 0 8px 24px var(--shadow);
  --elev-3: 0 12px 32px var(--shadow);

  /* motion — the site's one timing language (Phase 5). Every UI
     transition/animation routes through these:
       fast = micro feedback (borders, chevrons, small fades)
       base = standard UI motion (hovers, toasts, dropdowns, --transition)
       slow = entrances (cardIn and friends)
     Deliberate outliers that stay literal: the ambient loops (float,
     symbolFloat, motifFloat, nsubFloat, scrollBounce, floatY, pulseDot,
     skelShimmer, spin) are scenery rather than UI timing, and .fade-in's
     0.8s scroll reveal is tuned to scroll speed, not interaction speed. */
  --dur-fast: 0.15s;
  --dur-base: 0.3s;
  --dur-slow: 0.5s;
  --ease-standard: ease;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-pop: cubic-bezier(0.2, 0.9, 0.3, 1.15);  /* slight overshoot — modal/panel pops */
}

html { scroll-behavior: smooth; }

/* ============================================================
   ACCESSIBILITY BASELINE

   Keyboard focus: :focus-visible fires only on keyboard/assistive-tech
   focus, never on mouse click, so mouse users see no change anywhere.
   The ring follows the brand via --accent-solid. Inputs that style plain
   :focus (search, forms, sudo modal) keep their treatment; this is the
   floor under everything else — buttons, links, cards, toggles.
   ============================================================ */
:focus-visible {
  outline: 3px solid var(--accent-solid);
  outline-offset: 2px;
  border-radius: 2px;   /* keeps the ring from looking clipped on square corners */
}

/* Skip link — first tabbable element on every page. Parked above the
   viewport, drops in on keyboard focus. z-index above the sudo modal
   (5000), the highest layer in the file. */
.skip-link {
  position: fixed;
  top: -80px;
  left: 16px;
  z-index: 6000;
  padding: 12px 22px;
  border-radius: 12px;
  background: var(--accent-solid);
  color: var(--text-on-accent);
  font-weight: 700;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
  /* Deliberately NO transition on top: Chromium (headed and headless) leaves
     a transition on a fully-offscreen fixed element permanently pending —
     startTime never assigned, currentTime pinned at 0 — so the link never
     arrives. A transition here also outranks even inline !important while
     "running", which made this maddening to debug. Instant jump is standard
     for skip links anyway. */
}
.skip-link:focus-visible { top: 12px; }

/* Visually-hidden but readable by assistive tech. Used for live-region
   announcers (e.g. the exam timer's threshold callouts) that must reach a
   screen reader without occupying layout. Standard clip-rect technique. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

body {
  font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  background: var(--bg-primary);
  color: var(--text-primary);
  transition: var(--transition);
  overflow-x: hidden;
}

a { text-decoration: none; color: inherit; }
ul { list-style: none; }

.btn {
  display: inline-block;
  padding: 15px 36px;
  border-radius: 50px;
  font-weight: 600;
  font-size: 15px;
  cursor: pointer;
  border: none;
  transition: var(--transition);
  /* Default for gradient-filled buttons; see the note on the indigo
     .btn-primary rule. CAREFUL: this does NOT protect a rule that sets the
     `background` SHORTHAND — that resets every background-* longhand,
     including this one. Any rule giving a button a gradient must repeat
     background-origin: border-box itself. */
  background-origin: border-box;
}
/* Primary button: solid moss green with paper shadow by day;
   glowing warm-gold "lantern" with dark text by night. */
.btn-primary {
  background: var(--accent-solid);
  border: 2px solid var(--accent-solid-border);
  color: var(--text-on-accent);
}
/* Hover must read without motion: reduced-motion visitors get the state
   change with no glide, so the fill itself brightens — visible even when
   the 2px lift snaps instantly. brightness() also works on the indigo
   gradient and the night lantern, where a colour swap wouldn't. */
.btn-primary:hover { transform: translateY(-2px); filter: brightness(1.09); }
[data-mode="dark"] .home .btn-primary {
  background: linear-gradient(135deg, var(--lantern-1), var(--lantern-2));
  background-origin: border-box;   /* see the indigo rule above */
  border-color: var(--lantern-border);
  color: var(--lantern-ink);
  box-shadow: 0 0 26px var(--lantern-glow);
}
[data-mode="dark"] .home .btn-primary:hover {
  box-shadow: 0 0 40px var(--lantern-glow-hover);
}
.btn-outline {
  background: transparent;
  border: 2px solid var(--accent-1);
  color: var(--text-primary);
}
.btn-outline:hover { background: var(--accent-1); color: var(--outline-hover-ink); }

/* ===== NAVBAR =====
   Transparent at the very top of EVERY page (pages get full-bleed
   background art like the homepage), frosted as soon as the user scrolls
   (top-hint kicks in at 8px — see updateNav in script.js) so content never
   shows through the bar. */
.navbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 1000;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: var(--transition);
}
.navbar.top-hint,
.navbar.scrolled {
  background: var(--nav-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom-color: var(--card-border);
}

/* NEW: shrunken / elevated state applied via JS once the user scrolls */
.navbar.scrolled {
  box-shadow: 0 8px 30px var(--shadow);
  border-bottom-color: transparent;
}
.navbar.scrolled .nav-container {
  padding: 10px 24px;
}

.nav-container {
  max-width: 1600px;
  margin: 0 auto;
  padding: 16px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  transition: var(--transition);
}
.logo img {
    height: 70px;
    width: auto;
    display: block;
}

.logo{
    display:flex;
    align-items:center;
    gap:12px;
    text-decoration:none;
}

.logo img{
    height:55px;
    width:auto;
    display:block;
}

/* Per-brand logo mark. The book is a raster asset, so it can't follow the
   accent tokens — each brand ships its own file and we swap the image here
   rather than editing the <img src> on 30 pages. The URL is resolved against
   THIS stylesheet, so it works at any page depth (including notes/<x>/<y>.html
   and 404.html, which sit at other levels). Meadow keeps the src in the HTML,
   so a browser that ignores `content` on <img> degrades to the green mark. */
[data-theme="indigo"] .logo img {
    content: url(images/Logos/logo-indigo.png);
}

.logo-text{
    font-size:2rem;
    font-weight:700;
    color:var(--text-primary);
    /* inherits the body's system stack — see the TYPEFACE note at the top */
    line-height:1;
}

.logo-highlight{
    color:var(--accent-2);
}



.logo span { background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.nav-links { display: flex; gap: 32px; }
.nav-link { font-weight: 500; color: var(--text-secondary); position: relative; transition: var(--transition); }
.nav-link:hover { color: var(--text-primary); }
.nav-link::after {
  content: ''; position: absolute; bottom: -6px; left: 0; width: 0; height: 2px;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); transition: var(--transition);
}
.nav-link:hover::after { width: 100%; }

/* NEW: highlights the nav link matching the section currently in view */
.nav-link.active { color: var(--text-primary); }
.nav-link.active::after { width: 100%; }

.nav-actions { display: flex; align-items: center; gap: 12px; }
.icon-btn {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  width: 40px; height: 40px;
  border-radius: 50%;
  cursor: pointer;
  font-size: 18px;
  display: flex; align-items: center; justify-content: center;
  transition: var(--transition);
}
.icon-btn:hover { transform: scale(1.1); }
/* "Install app". Injected into .nav-actions by script.js only when the browser
   says installing is possible (and never inside the installed app), so it is
   absent from every page's markup by design. Accent-tinted because it offers
   something rather than toggling something. */
.install-btn {
  border-color: var(--accent-1);
  color: var(--accent-1);
  font-size: 20px; line-height: 1;
}
.hamburger { display: none; }

/* ===== HERO ===== */
.hero {
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  text-align: center;
  position: relative;
  overflow: hidden;
  padding: 120px 24px 60px;
}
.hero-blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.5;
  animation: float 8s ease-in-out infinite;
  z-index: 0;
}
.blob-1 { width: 400px; height: 400px; background: var(--accent-1); top: -100px; left: -100px; }
.blob-2 { width: 350px; height: 350px; background: var(--accent-2); bottom: -100px; right: -100px; animation-delay: 2s; }
@keyframes float {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50% { transform: translate(30px, -30px) scale(1.1); }
}

/* Floating math symbols — HOMEPAGE HERO ONLY, every brand (owner call,
   2026-07-17): the one place the site says "math" in its scenery. All
   other heroes carry the brand motifs below instead. */
.hero-symbols {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
}
.hero-symbol {
  position: absolute;
  font-size: 2.2rem;
  font-weight: 700;
  color: var(--accent-1);
  opacity: 0.15;
  animation: symbolFloat 9s ease-in-out infinite;
}
@keyframes symbolFloat {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50% { transform: translateY(-22px) rotate(8deg); }
}

/* ===== Brand signature motifs =====
   The floating shapes behind CARD-hero text — each brand's visual
   personality (THEMES.md "structural signatures"). One markup pattern
   (.hero-motifs > 7 empty .hero-motif spans), injected into .subject-hero
   by the motif block in script.js; the BRAND decides what the shape is and
   the accent tokens decide its colours, so brand and mode switches
   recolour/reshape with zero markup changes:
     meadow + autumn — a drifting leaf (border-radius shape)
     indigo          — a four-point sparkle (clip-path)
   The homepage hero deliberately does NOT use these — it keeps the math
   symbols above. Positions below stay clear of text and of the
   breadcrumb's top-left corner. */
.hero-motifs {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
}
.hero-motif {
  position: absolute;
  width: 18px;
  height: 18px;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  border-radius: 0 60% 55% 60%;   /* leaf */
  opacity: 0.3;
  transform: rotate(var(--motif-rot, 0deg)) scale(var(--motif-scale, 1));
  animation: motifFloat 10s ease-in-out infinite;
}
[data-theme="indigo"] .hero-motif,
[data-theme="azure"] .hero-motif {
  border-radius: 0;
  clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%);   /* sparkle */
}
/* The static transform above is the resting frame: the keyframes carry the
   same rotate/scale so the sway animates around it, and the global
   reduced-motion block freezes elements back onto it. */
@keyframes motifFloat {
  0%, 100% { transform: translateY(0) rotate(var(--motif-rot, 0deg)) scale(var(--motif-scale, 1)); }
  50% { transform: translateY(-16px) rotate(calc(var(--motif-rot, 0deg) + 10deg)) scale(var(--motif-scale, 1)); }
}
/* Card heroes (subject pages + the four hubs): flanks only — the centre
   column holds the text and the breadcrumb owns the top-left corner, so
   left-side motifs start below it. */
.subject-hero .hero-motif:nth-child(1) { top: 52%; left: 4%;   --motif-rot: 35deg; }
.subject-hero .hero-motif:nth-child(2) { top: 76%; left: 9%;   --motif-rot: -25deg; --motif-scale: 0.75; animation-delay: 1.4s; }
.subject-hero .hero-motif:nth-child(3) { top: 34%; left: 14%;  --motif-rot: 75deg;  --motif-scale: 0.6;  animation-delay: 0.6s; }
.subject-hero .hero-motif:nth-child(4) { top: 18%; right: 5%;  --motif-rot: -60deg; --motif-scale: 0.85; animation-delay: 1s; }
.subject-hero .hero-motif:nth-child(5) { top: 55%; right: 10%; --motif-rot: 15deg;  --motif-scale: 1.05; animation-delay: 2.2s; }
.subject-hero .hero-motif:nth-child(6) { top: 78%; right: 16%; --motif-rot: -10deg; --motif-scale: 0.7;  animation-delay: 1.8s; }
.subject-hero .hero-motif:nth-child(7) { top: 84%; right: 3%;  --motif-rot: 50deg;  --motif-scale: 0.9;  animation-delay: 0.4s; }
/* As the flanks narrow, drop the innermost shapes first, then all of them —
   on phones the text column spans the whole card. */
@media (max-width: 1100px) {
  .subject-hero .hero-motif:nth-child(3),
  .subject-hero .hero-motif:nth-child(6) { display: none; }
}
@media (max-width: 768px) {
  .subject-hero .hero-motif { display: none; }
}

.hero-content { max-width: 720px; z-index: 2; position: relative; }
.hero-badge {
  display: inline-block;
  padding: 8px 18px;
  border-radius: 50px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 24px;
  backdrop-filter: blur(10px);
}
.hero h1 { font-size: 52px; font-weight: 800; line-height: 1.2; margin-bottom: 20px; }

/* NEW: animated gradient highlight for the key word in the headline
   (reuses the existing --accent-1 / --accent-2 palette, no new colors) */
.hero-highlight {
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero p { font-size: 18px; color: var(--text-secondary); margin-bottom: 32px; }
.hero-buttons { display: flex; gap: 16px; justify-content: center; margin-top: 30px; flex-wrap: wrap; }

/* NEW: bouncing scroll indicator at the bottom of the hero */
.hero-scroll {
  position: absolute;
  bottom: 30px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  color: var(--text-secondary);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  z-index: 2;
  animation: scrollBounce 2s ease-in-out infinite;
  transition: var(--transition);
}
.hero-scroll:hover { color: var(--accent-1); }
.hero-scroll .arrow { font-size: 18px; }
@keyframes scrollBounce {
  0%, 100% { transform: translate(-50%, 0); }
  50% { transform: translate(-50%, 8px); }
}

/* ===== SECTION SHARED ===== */
.section-header { text-align: center; max-width: 600px; margin: 0 auto 48px; padding: 0 24px; }
.section-header h2 { font-size: 36px; font-weight: 800; margin-bottom: 12px; }
.section-header p { color: var(--text-secondary); font-size: 16px; }

/* ===== WHY LEARNTOMATH (features) ===== */
.features-section {
  padding: 100px 24px;
  background: var(--bg-secondary);
  transition: var(--transition);
}
.features-grid {
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 32px;
}
.feature-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 24px;
  padding: 36px 32px;
  box-shadow: 0 6px 20px var(--shadow);
  transition: var(--transition);
  display: flex;
  flex-direction: column;
}
.feature-card:hover {
  transform: translateY(-8px);
  box-shadow: 0 20px 48px var(--shadow);
  border-color: var(--accent-1);
}
/* Ring icon: circular field-journal badge — parchment fill, green ring */
.feature-icon {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--icon-ring);
  background: var(--icon-tint);
  color: var(--icon-ink);
  margin-bottom: 18px;
  transition: var(--transition);
}
.feature-icon svg { width: 24px; height: 24px; }
.feature-card:hover .feature-icon { transform: rotate(-6deg) scale(1.08); border-color: var(--accent-2); }
.feature-card:nth-child(2) { transition-delay: 0.05s; }
.feature-card:nth-child(3) { transition-delay: 0.1s; }
.feature-card:nth-child(4) { transition-delay: 0.15s; }
.feature-card:nth-child(5) { transition-delay: 0.2s; }
.feature-card:nth-child(6) { transition-delay: 0.25s; }
.feature-card h3 { font-size: 18px; font-weight: 700; margin-bottom: 10px; }
.feature-card p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; }

/* ===== EXPLORE MATHEMATICS (subjects) ===== */
.subjects-section { padding: 100px 24px; max-width: 1200px; margin: 0 auto; }
.subjects-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 24px;
}
.subject-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 20px;
  padding: 20px;
  text-align: left;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  transition: var(--transition);
  animation: cardIn var(--dur-slow) var(--ease-standard) both;
}
.subject-card:hover {
  transform: translateY(-6px);
  box-shadow: 0 16px 40px var(--shadow);
}
/* .subject-icon holds a single inline <svg> (24x24 Lucide-style icon).
   Sizing/colour live here so any icon dropped in automatically matches:
   the svg's stroke references the shared #icon-gradient defined once
   near the top of <body>, which follows --accent-1 / --accent-2. */
.subject-icon {
  width: 2.2rem;
  height: 2.2rem;
  margin-bottom: 12px;
  transition: var(--transition);
}
.subject-icon svg {
  width: 100%;
  height: 100%;
  display: block;
  stroke: url(#icon-gradient);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.subject-card:hover .subject-icon { transform: scale(1.15); }
.subject-card:nth-child(2) { transition-delay: 0.04s; }
.subject-card:nth-child(3) { transition-delay: 0.08s; }
.subject-card:nth-child(4) { transition-delay: 0.12s; }
.subject-card:nth-child(5) { transition-delay: 0.16s; }
.subject-card:nth-child(6) { transition-delay: 0.2s; }
.subject-card:nth-child(7) { transition-delay: 0.24s; }
.subject-card:nth-child(8) { transition-delay: 0.28s; }
.subject-card h3 { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
.subject-card p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 18px; flex-grow: 1; }
.subject-explore {
  align-self: flex-start;
  padding: 9px 18px;
  border-radius: 50px;
  font-size: 13px;
  font-weight: 600;
  border: 1px solid var(--accent-1);
  color: var(--text-primary);
  transition: var(--transition);
}
.subject-explore:hover { background: var(--accent-1); color: var(--text-on-accent); }

/* ===== NOTES ===== */
.notes-section { padding: 100px 24px; max-width: 1200px; margin: 0 auto; }
/* Hub grids sit under a .subject-hero card that already carries the page's top
   spacing, so the 100px above is a second helping.
   `body.has-cont-strip #exams` (further down) already shrinks it to 40px when
   the "Continue" strip renders — the strip is meant to FILL this gap, not add
   to it. What was missing is the other half: a student who has sat nothing
   gets no strip (.cont-strip:empty collapses), and the full 100px stayed as a
   viewport-wide hole. notes.html has zeroed this since it shipped
   (.notes-hub .notes-section, below); the exams hub never did. The
   has-cont-strip rule is more specific, so it still wins when there IS a
   strip. */
#exams.notes-section { padding-top: 0; }
#notes-preview .section-header { margin-bottom: 24px; }
.controls { display: flex; flex-direction: column; align-items: center; gap: 24px; margin-bottom: 48px; }
.search-input {
  width: 100%; max-width: 480px;
  padding: 14px 20px;
  border-radius: 50px;
  border: 1px solid var(--card-border);
  background: var(--input-bg);
  color: var(--text-primary);
  font-size: 15px;
  outline: none;
  transition: var(--transition);
}
.search-input:focus { border-color: var(--accent-1); box-shadow: 0 0 0 4px var(--shadow); }
.filter-buttons { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.filter-btn {
  padding: 10px 20px;
  border-radius: 50px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
  transition: var(--transition);
}
.filter-btn:hover { color: var(--text-primary); }
.filter-btn.active {
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  color: var(--text-on-accent);
  border-color: transparent;
  background-origin: border-box;   /* see .btn — stops the end-colour rim */
}

.notes-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 380px));
  gap: 32px;
  justify-content: center;
}
.note-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 24px;
  padding: 36px 32px;
  box-shadow: 0 6px 20px var(--shadow);
  transition: var(--transition);
  display: flex;
  flex-direction: column;
  gap: 16px;
  animation: cardIn var(--dur-slow) var(--ease-standard) both;
}
.note-card:hover { transform: translateY(-8px); box-shadow: 0 20px 48px var(--shadow); border-color: var(--accent-1); }
.note-tag {
  align-self: flex-start;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  padding: 5px 12px;
  border-radius: 50px;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  color: var(--text-on-accent);
}
.note-card h3 { font-size: 19px; font-weight: 700; }
.note-card p { font-size: 14px; color: var(--text-secondary); flex-grow: 1; }
.note-download {
  align-self: flex-start;
  padding: 10px 20px;
  border-radius: 50px;
  font-size: 13px;
  font-weight: 600;
  border: 1px solid var(--accent-1);
  color: var(--text-primary);
  transition: var(--transition);
}
.note-download:hover { background: var(--accent-1); color: var(--text-on-accent); }
.no-results { text-align: center; color: var(--text-secondary); margin-top: 40px; font-size: 16px; }

@keyframes cardIn {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ===== ABOUT ===== */
.about-section { padding: 100px 24px; background: var(--bg-secondary); }
.about-container {
  max-width: 1200px; margin: 0 auto;
  display: grid; grid-template-columns: 1.2fr 1fr; gap: 60px; align-items: center;
}
.about-text h2 { font-size: 36px; font-weight: 800; margin-bottom: 20px; }
.about-text p { color: var(--text-secondary); margin-bottom: 16px; line-height: 1.7; }
.about-stats { display: flex; gap: 20px; margin-top: 32px; flex-wrap: wrap; }
.stat-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  padding: 20px 28px;
  border-radius: 16px;
  text-align: center;
  backdrop-filter: blur(10px);
}
.stat-card h3 { font-size: 28px; font-weight: 800; background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.stat-card p { font-size: 13px; color: var(--text-secondary); margin-top: 4px; }
.about-visual { display: flex; justify-content: center; }
.glass-card {
  width: 280px; height: 280px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 30px;
  display: flex; align-items: center; justify-content: center;
  font-size: 32px;
  box-shadow: 0 20px 50px var(--shadow);
}
.floating { animation: floatY 4s ease-in-out infinite; }
@keyframes floatY {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-16px); }
}

/* ===== CONTACT ===== */
.contact-section { padding: 100px 24px; max-width: 700px; margin: 0 auto; }
.contact-profile-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 24px;
  padding: 40px;
  max-width: 600px;
  margin: 0 auto;
  box-shadow: 0 10px 30px var(--shadow);
}
.contact-profile-header {
  text-align: center;
  margin-bottom: 30px;
  padding-bottom: 24px;
  border-bottom: 1px solid var(--card-border);
}
.contact-title-area h2 {
  font-size: 28px;
  font-weight: 800;
  margin-bottom: 4px;
}
.contact-role {
  color: var(--accent-1);
  font-weight: 600;
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: 1px;
}
.contact-details-grid {
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.contact-detail-item {
  display: flex;
  align-items: flex-start;
  gap: 16px;
}
.detail-icon {
  font-size: 24px;
  margin-top: 2px;
  background: var(--bg-secondary);
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  border: 1px solid var(--card-border);
}
.detail-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.detail-text strong {
  font-size: 13px;
  text-transform: uppercase;
  color: var(--text-secondary);
  letter-spacing: 0.5px;
}
.detail-text span, .detail-text a {
  font-size: 16px;
  color: var(--text-primary);
  font-weight: 500;
  line-height: 1.5;
}
.detail-text a {
  color: var(--accent-1);
  text-decoration: none;
  transition: var(--transition);
}
.detail-text a:hover {
  color: var(--accent-2);
  text-decoration: underline;
}
.contact-form {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 24px;
  padding: 40px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.form-group { display: flex; flex-direction: column; gap: 8px; }
.form-group label { font-size: 14px; font-weight: 600; }
.form-group input, .form-group textarea {
  padding: 14px 16px;
  border-radius: 12px;
  border: 1px solid var(--card-border);
  background: var(--input-bg);
  color: var(--text-primary);
  font-size: 14px;
  font-family: inherit;
  outline: none;
  resize: vertical;
  transition: var(--transition);
}
.form-group input:focus, .form-group textarea:focus { border-color: var(--accent-1); box-shadow: 0 0 0 4px var(--shadow); }
.form-success {
  display: none;
  color: var(--success-msg);
  font-weight: 600;
  font-size: 14px;
  text-align: center;
}
.form-success.show { display: block; animation: cardIn var(--dur-slow) var(--ease-standard); }

/* ===== FOOTER ===== */
.footer { background: var(--bg-secondary); border-top: 1px solid var(--card-border); padding: 60px 24px 24px; }
.footer-container {
  max-width: 1200px; margin: 0 auto;
  display: flex; justify-content: space-between; flex-wrap: wrap; gap: 30px;
  margin-bottom: 30px;
}
.footer-brand h3 { font-size: 22px; font-weight: 800; margin-bottom: 8px; }
.footer-brand p { color: var(--text-secondary); font-size: 14px; }
.footer-links, .footer-social { display: flex; flex-direction: column; gap: 10px; }
.footer-links a, .footer-social a { color: var(--text-secondary); font-size: 14px; transition: var(--transition); }
.footer-links a:hover, .footer-social a:hover { color: var(--accent-1); }
.footer-bottom { text-align: center; color: var(--text-secondary); font-size: 13px; border-top: 1px solid var(--card-border); padding-top: 20px; max-width: 1200px; margin: 0 auto; }

/* ===== SCROLL TOP ===== */
.scroll-top {
  position: fixed;
  bottom: 30px; right: 30px;
  width: 48px; height: 48px;
  border-radius: 50%;
  border: none;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  color: var(--text-on-accent);
  font-size: 20px;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  opacity: 0;
  visibility: hidden;
  transform: translateY(20px);
  transition: var(--transition);
  z-index: 999;
}
.scroll-top.show { opacity: 1; visibility: visible; transform: translateY(0); }
.scroll-top:hover { transform: translateY(-4px); }

/* ===== FADE-IN ===== */
.fade-in { opacity: 0; transform: translateY(24px); transition: all 0.8s ease; }
.fade-in.visible { opacity: 1; transform: translateY(0); }

/* ===== RESPONSIVE ===== */
@media (max-width: 900px) {
  .about-container { grid-template-columns: 1fr; }
  .about-visual { order: -1; }
}
@media (max-width: 768px) {
  .nav-links {
    position: absolute;
    top: 100%; left: 0; right: 0;
    background: var(--nav-bg);
    backdrop-filter: blur(14px);
    flex-direction: column;
    padding: 20px 24px;
    gap: 18px;
    border-bottom: 1px solid var(--card-border);
    transform: translateY(-150%);
    opacity: 0;
    /* Only the slide and fade animate. The shared `--transition` is
       `all 0.3s`, which also animated the backdrop-filter — so opening the
       menu ran a full-panel blur from 14px to 0 (the touch override sets it
       to none), the single heaviest thing a weak GPU could be asked to do on
       a tap. Naming the two cheap compositor properties keeps the motion and
       drops the cost. */
    transition: transform var(--dur-base) var(--ease-standard),
                opacity var(--dur-base) var(--ease-standard);
    pointer-events: none;
  }
  .nav-links.open { transform: translateY(0); opacity: 1; pointer-events: auto; }
  .hamburger { display: flex; }
  .hero h1 { font-size: 36px; }
  .hero p { font-size: 16px; }
  .section-header h2 { font-size: 28px; }

  /* NEW: scale down decorative hero elements on small screens */
  .hero-symbol { font-size: 1.5rem; opacity: 0.1; }
  .hero-scroll { bottom: 16px; }

  /* NEW: single-column layout for the features & subjects grids on mobile */
  .features-section, .subjects-section { padding: 70px 24px; }
  .features-grid, .subjects-grid { grid-template-columns: 1fr; }
}

/* ============================================================
   MULTI-PAGE PLATFORM ADDITIONS
   Everything below extends the original design system.
   No new colors/fonts introduced — reuses existing CSS variables.
   ============================================================ */

/* ===== NAV: current-page link state (for separate pages, no scroll-spy) ===== */
.nav-link.current { color: var(--text-primary); }
.nav-link.current::after { width: 100%; }

/* ===== NAV: login (right side) ===== */

.btn-login {
  padding: 10px 24px;
  border-radius: 50px;
  font-weight: 600;
  font-size: 14px;
  border: 2px solid var(--accent-1);
  color: var(--text-primary);
  background: transparent;
  cursor: pointer;
  transition: var(--transition);
  white-space: nowrap;
}
.btn-login:hover { background: var(--accent-1); color: var(--text-on-accent); }

/* ===== NAV: future account menu =====
   Once authentication exists, swap the .btn-login button for this
   .user-menu block (see commented markup in the header of every page).
   No other navbar markup needs to change. */
.user-menu { position: relative; }
.user-avatar-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px 6px 6px;
  border-radius: 50px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  cursor: pointer;
  font-weight: 600;
  font-size: 14px;
  color: var(--text-primary);
  transition: var(--transition);
}
.user-avatar-btn:hover { border-color: var(--accent-1); }
.user-avatar-circle {
  position: relative;   /* anchors .avatar-badge (unresolved corrections) */
  width: 30px; height: 30px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  color: var(--text-on-accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 700;
}
.user-dropdown {
  position: absolute;
  top: calc(100% + 12px);
  right: 0;
  min-width: 210px;
  background: var(--bg-secondary);
  border: 1px solid var(--card-border);
  border-radius: 16px;
  padding: 8px;
  box-shadow: 0 16px 40px var(--shadow);
  opacity: 0;
  transform: translateY(-8px);
  pointer-events: none;
  transition: var(--transition);
}
.user-menu.open .user-dropdown { opacity: 1; transform: translateY(0); pointer-events: auto; }
.user-dropdown a {
  display: block;
  padding: 10px 14px;
  border-radius: 10px;
  font-size: 14px;
  font-weight: 500;
  color: var(--text-secondary);
  transition: var(--transition);
}
.user-dropdown a:hover { background: var(--card-bg); color: var(--text-primary); }
.user-dropdown .dropdown-divider { height: 1px; background: var(--card-border); margin: 6px 4px; }

/* ===== ADMIN MODE (SUDO) MODAL ===== */
.sudo-overlay {
  position: fixed;
  inset: 0;
  z-index: 3000;
  background: var(--scrim);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.sudo-modal {
  width: 100%;
  max-width: 380px;
  background: var(--bg-secondary);
  border: 1px solid var(--card-border);
  border-radius: 20px;
  padding: 28px;
  box-shadow: 0 24px 60px var(--shadow);
}
.sudo-modal h3 { font-size: 20px; margin-bottom: 8px; color: var(--text-primary); }
.sudo-modal p { font-size: 14px; color: var(--text-secondary); margin-bottom: 18px; }
.sudo-modal input[type="password"] {
  width: 100%;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text-primary);
  font-size: 15px;
  margin-bottom: 12px;
}
.sudo-modal input[type="password"]:focus { outline: 2px solid var(--accent-1); border-color: transparent; }
.sudo-error {
  font-size: 13px;
  color: var(--danger-ink);
  background: var(--danger-tint);
  border: 1px solid var(--danger-border);
  border-radius: 8px;
  padding: 8px 12px;
  margin-bottom: 12px;
}
.sudo-actions { display: flex; gap: 10px; justify-content: flex-end; }

/* Two-factor block inside the sudo modal: visually distinct from the password
   field so the second step is impossible to miss. */
.sudo-2fa {
  margin: 4px 0 14px;
  padding: 14px;
  border: 1px dashed var(--card-border);
  border-radius: 12px;
  background: var(--card-bg);
}
.sudo-2fa label {
  display: block;
  font-size: 13px;
  font-weight: 700;
  margin-bottom: 8px;
  color: var(--text-primary);
}
.sudo-2fa-row { display: flex; gap: 8px; }
.sudo-2fa-row input {
  flex: 1;
  min-width: 0;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid var(--card-border);
  background: var(--bg-secondary);
  color: var(--text-primary);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 6px;
  text-align: center;
}
.sudo-2fa-row input:focus { outline: 2px solid var(--accent-1); border-color: transparent; }
.sudo-2fa-row .btn { white-space: nowrap; padding: 10px 14px; font-size: 13px; }
.sudo-2fa small { display: block; margin-top: 8px; font-size: 12px; color: var(--text-secondary); }

/* "Remember this device" checkbox under the 2FA block, and the note shown in
   its place once the device is already remembered. */
.sudo-remember {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0 0 14px;
  font-size: 13px;
  color: var(--text-secondary);
  cursor: pointer;
}
.sudo-remember input { margin-top: 2px; accent-color: var(--accent-1); }
.sudo-remembered-note {
  font-size: 13px;
  color: var(--text-secondary);
  margin: 0 0 14px;
}

/* ===== ACCESS TIER ===== */
/* Lock badge shown on content that needs an account or the Access plan */
.access-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border-radius: 20px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.3px;
  background: linear-gradient(135deg, var(--gold-1), var(--gold-2));
  color: var(--gold-ink);
  box-shadow: 0 2px 8px var(--gold-glow);
  white-space: nowrap;
}
.access-badge.badge-members {
  background: var(--card-bg);
  color: var(--accent-1);
  border: 1px solid var(--card-border);
  box-shadow: none;
}

/* "Access Notes & Exams" upsell modal — two side-by-side panels.
   No backdrop-filter here: blurring the whole viewport is re-computed every
   frame while hero blobs/motifs animate behind the overlay, which on weaker
   GPUs stalls the page for seconds and makes the close button feel dead. A
   darker plain scrim gives the same separation at zero per-frame cost. */
.access-overlay {
  position: fixed; inset: 0; z-index: 3000;
  background: var(--scrim-strong);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  animation: accessFadeIn var(--dur-fast) var(--ease-out);
}
.access-modal {
  /* Defaults to the classic 720px dialog, but the native grip in the
     bottom-right corner (resize: both) lets anyone drag it larger or
     smaller, up to the viewport minus the overlay padding. */
  width: min(720px, 100%);
  min-width: min(320px, 100%);
  max-width: calc(100vw - 48px);
  resize: both;
  max-height: 94vh; overflow-y: auto;
  /* Flex column so a drag-resized height flows into .access-panels and the
     inner boxes stretch with the dialog instead of leaving a blank strip. */
  display: flex;
  flex-direction: column;
  background: var(--bg-secondary);
  border: 1px solid var(--card-border);
  border-radius: 22px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35);
  position: relative;
  animation: accessPopIn var(--dur-base) var(--ease-pop);
}
@keyframes accessFadeIn { from { opacity: 0; } }
@keyframes accessPopIn { from { opacity: 0; transform: translateY(10px) scale(0.97); } }
/* (reduced-motion handling is global — see the block at the end of this file) */
.access-modal-header {
  flex-shrink: 0;   /* never squashed by a drag-shrunk dialog — scroll instead */
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  padding: 18px 26px 14px;
  text-align: center;
  color: var(--text-on-accent);
  position: relative;
}
.access-modal-header h3 { font-size: 20px; font-weight: 800; margin-bottom: 2px; }
.access-modal-header p { font-size: 12.5px; opacity: 0.9; }
.access-close {
  position: absolute; top: 10px; right: 12px;
  background: rgba(255,255,255,0.18);
  border: none; color: var(--text-on-accent);
  width: 26px; height: 26px; border-radius: 50%;
  font-size: 14px; cursor: pointer; line-height: 1;
}
/* Invisible extension of the click target — the visible ✕ stays 26px. */
.access-close::after {
  content: '';
  position: absolute; inset: -8px;
  border-radius: 50%;
}
.access-close:hover { background: rgba(255,255,255,0.3); }

/* The two panels: online (left) and offline (right). Stack on narrow screens.
   Both are highlighted — gold for the paid path, green for the free offline
   unlock — so neither audience can miss theirs. */
.access-panels {
  flex: 1 0 auto;   /* absorb drag-resize height; grid stretch grows the panels */
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  padding: 18px;
  position: relative;   /* anchors the "or" chip */
}
@media (max-width: 620px) {
  .access-panels { grid-template-columns: 1fr; }
  .access-or { display: none; }   /* stacked panels: 50%/50% no longer sits between them */
}
/* Optional admin-authored line spanning under BOTH panels. flex-shrink: 0 so a
   drag-resize stretches the panels above it, not this line. */
.access-footer {
  flex-shrink: 0;
  text-align: center;
  font-size: 12.5px;
  color: var(--text-secondary);
  padding: 0 18px 16px;
  margin-top: -4px;   /* tuck against the panel grid's 18px bottom padding */
}
.access-or {
  position: absolute;   /* out of grid flow — must not become a third cell */
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--bg-secondary);
  border: 1px solid var(--card-border);
  font-size: 12px; font-weight: 700; font-style: italic;
  color: var(--text-secondary);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
  z-index: 1;
}
.access-panel {
  display: flex;
  flex-direction: column;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 16px;
  padding: 16px 18px 18px;
}
.access-panel--online { border-color: var(--gold-2); box-shadow: 0 6px 20px var(--gold-glow); }
.access-panel--offline {
  border-color: var(--success);
  box-shadow: 0 6px 20px rgba(16, 185, 129, 0.22);
}
/* Corner flags: what each path IS, at a glance. */
.access-flag {
  align-self: center;
  margin: -26px auto 8px;   /* rides the panel's top border */
  padding: 3px 12px;
  border-radius: 20px;
  font-size: 11px; font-weight: 800;
  letter-spacing: 0.3px;
  white-space: nowrap;
}
.access-flag--gold {
  background: linear-gradient(135deg, var(--gold-1), var(--gold-2));
  color: var(--gold-ink);
  box-shadow: 0 3px 10px var(--gold-glow);
}
.access-flag--green {
  background: var(--success);
  color: #fff;
  box-shadow: 0 3px 10px rgba(16, 185, 129, 0.35);
}
.access-panel-head { text-align: center; margin-bottom: 10px; }
.access-panel-head h4 { font-size: 15.5px; font-weight: 800; color: var(--text-primary); margin-bottom: 6px; }
.access-price { text-align: center; margin: 2px 0 4px; }
.access-price .amount { font-size: 28px; font-weight: 800; color: var(--text-primary); }
.access-price .amount sup { font-size: 14px; font-weight: 700; }
.access-price .amount--free { color: var(--success); letter-spacing: 1px; }
.access-price .term { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
.access-blurb { font-size: 12.5px; color: var(--text-secondary); margin-top: 4px; }
.access-features { list-style: none; padding: 0; margin: 6px 0 14px; flex: 1; }
.access-features li {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 5px 0;
  font-size: 13px;
  color: var(--text-primary);
  border-bottom: 1px solid var(--card-border);
}
.access-features li:last-child { border-bottom: none; }
.access-features .tick {
  color: var(--accent-1);
  font-weight: 800;
  flex-shrink: 0;
  font-size: 12.5px;
}
.access-panel--online .tick { color: var(--gold-2); }
.access-panel--offline .tick { color: var(--success); }
.access-features small { display: block; color: var(--text-secondary); font-size: 11px; margin-top: 0; }
/* Plain admin-editable line under the benefit list — deliberately no tick. */
.access-features-note {
  font-size: 12px;
  color: var(--text-secondary);
  margin: -8px 0 12px;
}
.access-req-note {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 10px;
  padding: 10px 12px;
  border-radius: 10px;
  /* The panel's green (semantic --success) — with no label above it any more,
     the box itself has to read as "type your number here". */
  border: 1.5px solid var(--success);
  background: var(--success-tint);
  color: var(--text-primary);
  font-family: inherit;
  font-size: 13px;
}
.access-req-note::placeholder { color: var(--text-secondary); opacity: 1; }
.access-req-note:focus {
  outline: none;
  border-color: var(--success-ink);
  box-shadow: 0 0 0 3px var(--success-tint);
}
.access-req-note:disabled { opacity: 0.55; cursor: not-allowed; }
.access-cta {
  display: block;
  width: 100%;
  padding: 11px;
  border: none;
  border-radius: 12px;
  font-size: 14px;
  font-weight: 800;
  cursor: pointer;
  transition: var(--transition);
}
.access-cta--online {
  background: linear-gradient(135deg, var(--gold-1), var(--gold-2));
  color: var(--gold-ink);
  box-shadow: 0 8px 24px var(--gold-glow);
}
.access-cta--online:hover { transform: translateY(-2px); box-shadow: 0 12px 32px var(--gold-glow-hover); }
.access-cta--offline {
  /* Semantic green (--success is brand-independent): free = go. Pairs against
     the gold paid CTA so the two paths read differently at a glance. */
  background: var(--success);
  color: #fff;
  box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3);
}
.access-cta--offline:hover { transform: translateY(-2px); filter: brightness(1.06); }
/* Locked "Requested" state: still green (it's good news), but clearly inert. */
.access-cta--requested,
.access-cta--requested:hover {
  opacity: 0.65;
  cursor: default;
  transform: none;
  filter: none;
  box-shadow: none;
}
.access-note {
  text-align: center;
  font-size: 11px;
  color: var(--text-secondary);
  margin-top: 8px;
}

/* ===== LOCKED ("coming soon") POPUP =====
   The pre-launch variant: no panels and no price, just the admin's message and
   one request button. Narrower than the two-panel dialog and not resizable —
   there is nothing inside worth dragging open. */
.access-modal--locked {
  width: min(430px, 100%);
  resize: none;
}
.access-modal--locked .access-modal-header { padding: 24px 26px 20px; }
.access-modal--locked .access-modal-header p {
  font-size: 13px;
  line-height: 1.55;
  margin-top: 6px;
}
.access-locked { padding: 20px 26px 24px; }
.access-cta--locked {
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  color: var(--text-on-accent);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}
.access-cta--locked:hover { transform: translateY(-2px); filter: brightness(1.06); }
.access-locked .access-note { font-size: 11.5px; margin-top: 10px; }

/* ===== PAGE HERO (for inner pages: learn / notes / classes / exams / login) ===== */
.page-hero {
  padding: 128px 24px 52px;
  text-align: center;
  position: relative;
  overflow: hidden;
}
.page-hero .hero-blob { opacity: 0.35; }
.page-hero-content { position: relative; z-index: 2; max-width: 700px; margin: 0 auto; }
.page-hero .hero-badge { margin-bottom: 18px; }
.page-hero h1 { font-size: 40px; font-weight: 800; line-height: 1.2; margin-bottom: 16px; }
.page-hero p { font-size: 17px; color: var(--text-secondary); }

/* ===== PREVIEW SECTIONS (homepage) ===== */
.section-cta { text-align: center; margin-top: 44px; }
.preview-grid-3 { max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 380px)); gap: 32px; justify-content: center; }

/* ===== TESTIMONIALS ===== */
.testimonials-section { padding: 100px 24px; background: var(--bg-secondary); }
.testimonials-grid { max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px; }
.testimonial-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 20px;
  padding: 32px 28px;
  transition: var(--transition);
}
.testimonial-card:hover { transform: translateY(-6px); box-shadow: 0 16px 40px var(--shadow); }
.testimonial-stars { color: var(--accent-2); font-size: 14px; margin-bottom: 14px; letter-spacing: 2px; }
.testimonial-quote { font-size: 15px; color: var(--text-primary); line-height: 1.7; margin-bottom: 20px; }
.testimonial-person { display: flex; align-items: center; gap: 12px; }
.testimonial-avatar {
  width: 40px; height: 40px; border-radius: 50%;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  color: var(--text-on-accent); display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 15px; flex-shrink: 0;
}
.testimonial-name { font-size: 14px; font-weight: 700; }
.testimonial-role { font-size: 12px; color: var(--text-secondary); }

/* ===== FINAL CTA BANNER ===== */
/* Hero-style frosted card (owner call, 2026-07-17): the old solid accent
   gradient read as a wall of colour. Blobs + floating symbols live in
   .cta-banner-bg so the sign-in personalization (which replaces only
   #hallContent) never wipes them. The button stays the standard primary. */
.cta-banner {
  max-width: 1200px;
  margin: 0 auto 100px;
  padding: 64px 40px;
  border-radius: 32px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  text-align: center;
  color: var(--text-primary);
  position: relative;
  overflow: hidden;
}
.cta-banner-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  border-radius: inherit;
  z-index: 0;
}
.cta-banner .hero-blob { opacity: 0.3; }
.cta-banner .hero-symbol { font-size: 1.8rem; }
.cta-banner-content { position: relative; z-index: 2; }
.cta-banner h2 { font-size: 32px; font-weight: 800; margin-bottom: 12px; }
.cta-banner p { font-size: 16px; color: var(--text-secondary); margin-bottom: 28px; }
/* CTA banner button inherits the standard primary (moss by day, lantern by night) */

/* ===== LEARNING PATHS (learn.html) ===== */
.paths-section { padding: 100px 24px; max-width: 1200px; margin: 0 auto; }
.paths-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px; }
.path-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 20px;
  padding: 30px;
  transition: var(--transition);
}
.path-card:hover { transform: translateY(-6px); box-shadow: 0 16px 40px var(--shadow); border-color: var(--accent-1); }
.path-level {
  display: inline-block;
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;
  padding: 5px 12px; border-radius: 50px;
  background: var(--card-bg); border: 1px solid var(--accent-1); color: var(--accent-1);
  margin-bottom: 14px;
}
.path-card h3 { font-size: 19px; font-weight: 700; margin-bottom: 8px; }
.path-card p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 16px; }
.path-meta { display: flex; gap: 16px; font-size: 13px; color: var(--text-secondary); font-weight: 600; }

/* ===== COURSE CARDS (learn.html) ===== */
.courses-section { padding: 0 24px 100px; max-width: 1200px; margin: 0 auto; }
.course-card { position: relative; }
.course-level-badge {
  position: absolute; top: 28px; right: 28px;
  font-size: 11px; font-weight: 700; color: var(--text-secondary);
}

/* ===== ROADMAP (learn.html) ===== */
.roadmap-section { padding: 0 24px 100px; max-width: 900px; margin: 0 auto; }
.roadmap { display: flex; flex-direction: column; gap: 0; }
.roadmap-step { display: flex; gap: 24px; }
.roadmap-marker { display: flex; flex-direction: column; align-items: center; }
.roadmap-number {
  width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  color: var(--text-on-accent); display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 15px;
}
.roadmap-line { width: 2px; flex: 1; background: var(--card-border); margin: 4px 0; }
.roadmap-step:last-child .roadmap-line { display: none; }
.roadmap-body { padding-bottom: 40px; }
.roadmap-body h3 { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
.roadmap-body p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; }

/* ===== NOTES PAGE: categories + pdf preview ===== */
.categories-row { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; margin-bottom: 32px; }
.category-chip {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 18px; border-radius: 50px;
  background: var(--card-bg); border: 1px solid var(--card-border);
  font-size: 13px; font-weight: 600; color: var(--text-secondary);
}
.pdf-preview {
  height: 90px; border-radius: 12px;
  background: var(--input-bg); border: 1px dashed var(--card-border);
  display: flex; align-items: center; justify-content: center;
  font-size: 26px; color: var(--accent-1); margin-bottom: 4px;
}

/* ===== CLASSES PAGE ===== */
.classes-section { padding: 60px 24px; max-width: 1200px; margin: 0 auto; }
/* The first row under a hero is pulled right up against it — it carries no
   heading of its own, so a whole card sits above the fold on a laptop.
   (Classes hub + the recorded-collection page.) */
.classes-section--first { padding-top: 30px; }
/* On the Classes hub the switch has already spent that gap, so the rows it
   controls take a much smaller one. Sibling selector, so the rule can't
   reach the recorded-collection page, which has no switch. */
.classes-switch-wrap ~ .classes-section--first { padding-top: 10px; }

/* ----- Classes hub: the Live / Recorded switch (classes-switch.js) -----
   A segmented pill under the hero that swaps the two card rows. It stands in
   for the headings those rows used to carry, at a fraction of their height —
   an h2 plus a sub-line cost ~110px and pushed the first card off the fold.
   Every dimension here is deliberately tight for the same reason: the whole
   switch block (gap + pill + the row's own top padding) must stay near the
   30px of section padding it replaced, or the first card slides under the
   fold on a short laptop screen. The hero card's own 28px bottom margin
   collapses through .subject-page-wrap and already sits above this. */
.classes-switch-wrap {
  display: flex;
  justify-content: center;
  padding: 6px 24px 0;
}
/* `display: flex` beats the UA's `[hidden] { display: none }`, and this
   element is hidden by the `hidden` attribute whenever the recorded library
   is empty — without this the switch would show with a dead second tab. */
.classes-switch-wrap[hidden] { display: none; }
.classes-switch {
  display: inline-flex;
  gap: 4px;
  padding: 5px;
  border-radius: 999px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  box-shadow: 0 2px 14px var(--shadow);
}
.classes-switch-btn {
  padding: 7px 28px;
  border: none;
  border-radius: 999px;
  background: transparent;
  font-family: inherit;
  font-size: 14.5px;
  font-weight: 700;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-standard),
              background-color var(--dur-base) var(--ease-standard);
}
.classes-switch-btn:hover { color: var(--text-primary); }
/* background-origin so the gradient fills the pill, not just the padding box
   — the shorthand would reset it (see THEMES.md). */
.classes-switch-btn.is-active {
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2)) border-box;
  background-origin: border-box;
  color: var(--text-on-accent);
  box-shadow: 0 3px 12px var(--shadow);
}
@media (max-width: 480px) {
  .classes-switch-btn { padding: 8px 22px; font-size: 14px; }
}
.classes-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 380px)); gap: 24px; justify-content: center; }
.class-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 24px;
  padding: 36px 32px;
  box-shadow: 0 6px 20px var(--shadow);
  transition: var(--transition);
  position: relative;
  display: flex;
  flex-direction: column;
}
.class-card:hover { transform: translateY(-8px); box-shadow: 0 20px 48px var(--shadow); border-color: var(--accent-1); }
.live-badge {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;
  color: var(--live); margin-bottom: 12px;
}
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--live); animation: pulseDot 1.4s ease-in-out infinite; }
@keyframes pulseDot { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
.class-meta { display: flex; gap: 14px; font-size: 13px; color: var(--text-secondary); font-weight: 600; margin: 10px 0 16px; flex-wrap: wrap; }
.class-card h3 { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
.class-card p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 16px; }
.btn-register {
  display: inline-block; padding: 11px 26px; border-radius: 50px;
  font-size: 13px; font-weight: 600; color: var(--text-on-accent);
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  transition: var(--transition);
}
.btn-register:hover { transform: translateY(-2px); }

/* ===== WEEKLY SCHEDULE ===== */
.schedule-section { padding: 60px 24px 100px; max-width: 1200px; margin: 0 auto; }
.schedule-table { overflow-x: auto; border-radius: 20px; border: 1px solid var(--card-border); }
.schedule-grid { display: grid; grid-template-columns: 110px repeat(7, minmax(120px, 1fr)); min-width: 900px; }
.schedule-grid .schedule-cell {
  padding: 14px 12px; font-size: 13px; border-bottom: 1px solid var(--card-border); border-right: 1px solid var(--card-border);
  color: var(--text-secondary);
}
.schedule-grid .schedule-head { background: var(--card-bg); font-weight: 700; color: var(--text-primary); text-align: center; }
.schedule-grid .schedule-time { background: var(--card-bg); font-weight: 700; color: var(--text-primary); }
.schedule-grid .schedule-cell:last-child { border-right: none; }
.schedule-slot { background: var(--input-bg); border-radius: 10px; padding: 6px 8px; font-size: 11px; font-weight: 600; color: var(--accent-1); text-align: center; }

/* ===== EXAMS PAGE ===== */
.exams-section { padding: 60px 24px; max-width: 1200px; margin: 0 auto; }
.exam-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 20px;
  padding: 26px;
  display: flex; flex-direction: column; gap: 12px;
  transition: var(--transition);
}
.exam-card:hover { transform: translateY(-6px); box-shadow: 0 16px 40px var(--shadow); }
.exam-icon { font-size: 26px; }
.exam-card h3 { font-size: 17px; font-weight: 700; }
.exam-card p { font-size: 13px; color: var(--text-secondary); line-height: 1.6; flex-grow: 1; }
.exam-meta { font-size: 12px; font-weight: 700; color: var(--accent-1); }

.results-preview {
  max-width: 1200px; margin: 0 auto 100px; padding: 0 24px;
}
.results-card {
  background: var(--card-bg); border: 1px solid var(--card-border); backdrop-filter: blur(16px);
  border-radius: 24px; padding: 36px; display: flex; align-items: center; gap: 40px; flex-wrap: wrap;
}
.results-ring {
  width: 120px; height: 120px; border-radius: 50%; flex-shrink: 0;
  background: conic-gradient(var(--accent-1) 0% 78%, var(--card-border) 78% 100%);
  display: flex; align-items: center; justify-content: center;
}
.results-ring-inner {
  width: 92px; height: 92px; border-radius: 50%; background: var(--bg-secondary);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.results-ring-inner strong { font-size: 22px; }
.results-ring-inner span { font-size: 11px; color: var(--text-secondary); }
.results-text h3 { font-size: 20px; font-weight: 700; margin-bottom: 8px; }
.results-text p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; max-width: 480px; }

/* ===== LOGIN PAGE ===== */
.login-page {
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  position: relative; overflow: hidden;
  padding: 120px 24px 60px;
  background:
    radial-gradient(circle at 18% 12%, var(--auth-glow-1), transparent 52%),
    radial-gradient(circle at 84% 88%, var(--auth-glow-2), transparent 52%);
}
/* Secondary button (used by cancel actions, …) */
.btn-secondary {
  background: var(--input-bg);
  border: 2px solid var(--card-border);
  color: var(--text-primary);
}
.btn-secondary:hover { border-color: var(--accent-1); transform: translateY(-2px); }
.login-card {
  position: relative; z-index: 2;
  width: 100%; max-width: 420px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 24px;
  padding: 44px 36px;
  box-shadow: 0 20px 50px var(--shadow);
}
.login-card .logo { justify-content: center; margin-bottom: 24px; }
.login-card h1 { font-size: 24px; font-weight: 800; text-align: center; margin-bottom: 8px; }
.login-card .login-sub { text-align: center; color: var(--text-secondary); font-size: 14px; margin-bottom: 28px; }
/* Links inside the intro line (welcome page points to Beta Terms + Privacy)
   must read as links, not grey body text — they carry the legal pointer. */
.login-card .login-sub a { color: var(--accent-1); font-weight: 600; }
.login-form { display: flex; flex-direction: column; gap: 18px; }
.login-row-between { display: flex; align-items: center; justify-content: space-between; font-size: 13px; }
.checkbox-row { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); }
.checkbox-row input { accent-color: var(--accent-1); width: 15px; height: 15px; }
.forgot-link { color: var(--accent-1); font-weight: 600; }
.login-card .btn-primary { width: 100%; text-align: center; margin-top: 6px; }
.login-switch { text-align: center; font-size: 13px; color: var(--text-secondary); margin-top: 22px; }
.login-switch a { color: var(--accent-1); font-weight: 700; }

/* Auth card — roomy and tidy. A slightly tighter top offset keeps it high
   on the page; the box itself uses comfortable, uncramped spacing. */
.login-page { padding: 104px 24px 48px; }
.login-page .login-card { padding: 34px 36px; border-radius: 22px; }
.login-page .login-card .logo { margin-bottom: 16px; }
.login-page .login-card .logo img { height: 38px; }
.login-page .login-card h1 { font-size: 24px; }
.login-page .login-card .login-sub { margin-bottom: 22px; }
.login-page .login-form { gap: 15px; }
.login-page .form-group { gap: 7px; }
.login-page .form-group label { font-size: 14px; }
.login-page .form-group input { padding: 12px 16px; border-radius: 12px; }
.login-page .login-switch { margin-top: 18px; font-size: 13px; }
.login-page .form-group small { font-size: 12px; }
.login-page .login-card .btn-primary { padding: 13px 22px; margin-top: 4px; }

/* Contact page — roomy card. The footer may sit just below the fold on
   short screens, which is fine. */
.contact-hero { padding: 118px 24px 32px; }
.contact-hero h1 { font-size: 40px; margin-bottom: 14px; }
.contact-hero .hero-badge { margin-bottom: 18px; }
.contact-hero p { font-size: 16px; }
.contact-section-compact { padding: 40px 24px 70px; }
.contact-section-compact .contact-profile-card { padding: 38px 40px; }
.contact-section-compact .contact-profile-header { margin-bottom: 26px; padding-bottom: 22px; }
.contact-section-compact .contact-title-area h2 { font-size: 26px; }
.contact-section-compact .contact-details-grid { gap: 20px; }
.contact-section-compact .detail-icon { width: 46px; height: 46px; font-size: 22px; }

/* ===== FOOTER: informational link group ===== */
.footer-links-title, .footer-social-title { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-primary); margin-bottom: 4px; }
/* Footer brand mirrors the header: logo image + wordmark */
.footer .logo img { height: 38px; width: auto; display: block; }

/* ===== RESPONSIVE ADDITIONS ===== */
@media (max-width: 768px) {
  .page-hero { padding: 108px 24px 42px; }
  .page-hero h1 { font-size: 32px; }
  .btn-login { padding: 8px 16px; font-size: 13px; }
  .results-card { flex-direction: column; text-align: center; }
  .schedule-grid { min-width: 760px; }
  /* Logged in, the avatar pill carries the user's name (up to 120px) — with
     the full logo that overflows anything under ~615px and shoves the
     hamburger off-screen. Icon-only avatar is the standard mobile pattern;
     the name is one tap away in the dropdown. */
  .user-avatar-btn span { display: none; }
  .user-avatar-btn { padding: 6px; }
}
/* ===== NAVBAR FIT — nothing in the nav row can flex-shrink, so each tier
   below trims the widest rigid piece. The full brand block (55px mark +
   2rem wordmark ≈ 294px) plus the action buttons is intrinsically wider
   than any viewport under ~540px; without these rules the hamburger sat
   clipped off the right edge and the menu was unopenable on phones. ===== */
@media (max-width: 600px) {
  .nav-actions { gap: 8px; }
  .nav-container { padding: 12px 16px; }
  .logo img { height: 40px; }
  .logo-text { font-size: 1.25rem; }
}
@media (max-width: 420px) {
  /* Mark-only logo: the shrunk wordmark + theme toggle + Log In + hamburger
     still can't share 360px. The Log In text stays — hiding it (the old
     approach) left a bordered empty pill with no accessible name. */
  .logo-text { display: none; }
}


/* ============================================================
   NOTES SECTION REDESIGN
   New, self-contained classes only — nothing above this block
   was modified, so no other page is affected.
   ============================================================ */

/* ===== NOTES HOME: two large subject cards ===== */
.notes-subject-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 32px;
  max-width: 900px;
  margin: 0 auto;
  padding: 0 24px 100px;
}
.notes-subject-card {
  display: block;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 24px;
  padding: 32px 24px;
  text-align: center;
  text-decoration: none;
  color: inherit;
  transition: var(--transition);
}
.notes-subject-card:hover {
  transform: translateY(-6px);
  box-shadow: 0 0 30px var(--accent-1);
  border-color: var(--accent-1);
}
/* .notes-subject-icon holds a single inline <svg> (24x24 Lucide-style icon),
   centred above the card title. Swap the whole svg to change the icon —
   sizing/colour are handled here so the replacement stays consistent. */
.notes-subject-icon {
  display: flex;
  justify-content: center;
  width: 48px;
  height: 48px;
  margin: 0 auto 20px;
}
.notes-subject-icon svg {
  width: 100%;
  height: 100%;
  display: block;
  stroke: url(#icon-gradient);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.notes-subject-card h3 { font-size: 24px; font-weight: 800; margin-bottom: 12px; }
.notes-subject-card p { font-size: 15px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 22px; }
.notes-subject-arrow { font-weight: 700; color: var(--accent-1); font-size: 14px; }

/* ===== SUBJECT PAGE: large horizontal topic cards =====
   To add a new topic later, duplicate one .topic-card-horizontal
   block inside .topics-list — see the HTML comment markers. */
.topics-list {
  display: flex;
  flex-direction: column;
  gap: 24px;
  max-width: 900px;
  margin: 0 auto;
  padding: 0 24px 100px;
}
.topic-card-horizontal {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 20px;
  padding: 32px 36px;
  text-decoration: none;
  color: inherit;
  transition: var(--transition);
}
.topic-card-horizontal:hover {
  transform: translateY(-4px);
  box-shadow: 0 16px 40px var(--shadow);
  border-color: var(--accent-1);
}
.topic-card-info h3 { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
.topic-card-info p { font-size: 14px; color: var(--text-secondary); }
.topic-card-arrow { font-size: 22px; color: var(--accent-1); flex-shrink: 0; }

/* ===== TOPIC PAGE: description + PDF viewer ===== */
.topic-detail-section {
  max-width: 900px;
  margin: 0 auto;
  padding: 0 24px 100px;
}
.topic-detail-desc {
  font-size: 16px;
  color: var(--text-secondary);
  line-height: 1.7;
  margin-bottom: 36px;
  max-width: 700px;
}
.reading-container {
    width: min(1600px, calc(100% - 2rem));
    margin: 0 auto;
}
.pdf-viewer-block {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 20px;
  padding: 24px;
}
.pdf-frame-placeholder {
  width: 100%;
  min-height: 480px;
  border-radius: 14px;
  border: 1px dashed var(--card-border);
  background: var(--input-bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  color: var(--text-secondary);
  text-align: center;
  padding: 24px;
}
.pdf-frame-placeholder .pdf-icon { font-size: 42px; }
.pdf-frame-placeholder p { font-size: 14px; max-width: 360px; }
.pdf-actions { margin-top: 22px; text-align: center; }
.pdf-download-btn { display: inline-block; }

/* ============================================================
   SUBJECT PAGES ONLY (subject.html + the hub-page heroes)
   Scoped under .subject-page — does not affect topic/PDF pages
   or any other section of the site.
   NOTE: this header comment once contained a glob path with "*" + "/"
   in sequence, which closed the comment early and made the CSS parser
   silently eat the .subject-page-wrap rule below — never put file
   globs inside CSS comments.
   ============================================================ */

.subject-page-wrap {
  /* Only top clearance for the ~88px fixed navbar lives here (the
     breadcrumb sits INSIDE the hero card, so nothing can slide behind
     the header). No max-width/side padding: the rule was dead for ages
     (see NOTE above), so the shipped design never had them — sections
     size themselves. */
  padding-top: 106px;
}

/* ----- Breadcrumb: pinned to the hero card's top-left corner -----
   (the viewer keeps its own standalone .vw-breadcrumb placement) */
.subject-hero .subject-breadcrumb {
  position: absolute;
  top: 16px;
  left: 22px;
  z-index: 2;
}

/* ----- Universal page hero (learn / classes / notes / exams) -----
   Hub pages reuse the subject page's breadcrumb + .subject-hero card via
   this modifier. Flex centering + min-height (instead of per-page padding)
   keeps every hero at the subject hero's exact height no matter what the
   card holds (search box, CTA buttons, or just text). */
.subject-page-wrap.hero-only { padding-bottom: 0; }
.subject-hero--page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 258px;
  padding: 34px 24px;
}
.subject-hero--page .hero-buttons { margin-top: 14px; }
.subject-breadcrumb-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  list-style: none;
  font-size: 14px;
  color: var(--text-secondary);
}
.subject-breadcrumb-list li {
  display: flex;
  align-items: center;
  gap: 8px;
}
.subject-breadcrumb-list li + li::before {
  content: ">";
  color: var(--text-secondary);
  opacity: 0.5;
  font-size: 13px;
  line-height: 1;
}
.subject-breadcrumb-list a {
  color: var(--text-secondary);
  transition: var(--transition);
}
.subject-breadcrumb-list a:hover {
  color: var(--accent-1);
}
.subject-breadcrumb-list li[aria-current="page"] {
  color: var(--text-primary);
  font-weight: 600;
}

/* ----- Section cards & exam tags (rendered by notes-render.js) ----- */
.subject-page #topic-list.lib-sec-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
  align-items: stretch;
}
.subject-page .lib-sec-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 16px;
  padding: 22px 24px 18px;
  box-shadow: 0 2px 12px var(--shadow);
  text-decoration: none;
  color: inherit;
  position: relative;
  overflow: hidden;
  transition: box-shadow var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-standard);
}
.subject-page .lib-sec-card::before {
  content: "";
  position: absolute;
  left: 0;
  top: 20%;
  bottom: 20%;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: linear-gradient(180deg, var(--accent-1), var(--accent-2));
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-standard);
}
.subject-page .lib-sec-card:hover::before { opacity: 1; }
.subject-page .lib-sec-card:hover {
  box-shadow: 0 12px 36px var(--shadow);
  transform: translateY(-2px);
}
.subject-page .lib-sec-card-top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}
.subject-page .lib-sec-card-title {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--text-primary);
  margin: 0;
  letter-spacing: -0.01em;
  line-height: 1.35;
}
.subject-page .lib-sec-card-desc {
  font-size: 0.875rem;
  color: var(--text-secondary);
  margin: 0;
  line-height: 1.6;
}
.subject-page .lib-sec-card-foot {
  margin-top: auto;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: 10px;
  border-top: 1px solid var(--card-border);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--accent-1);
}
.subject-page .lib-sec-card:hover .lib-btn-arrow { transform: translateX(3px); }

/* Exam tag pills — colour comes inline from the admin-managed tag */
.lib-tag-row {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
}
.lib-tag {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 20px;
  border: 1px solid;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.6;
  white-space: nowrap;
}

/* Done toggle on note cards + per-section completion chip */
.subject-page .lib-done-toggle {
  margin-left: auto;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1.5px solid var(--card-border);
  background: var(--bg-secondary);
  color: var(--text-secondary);
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
  transition: var(--transition);
}
.subject-page .lib-done-toggle:hover {
  border-color: var(--success);
  color: var(--success);
}
.subject-page .lib-done-toggle.is-done {
  background: var(--success);
  border-color: var(--success);
  color: var(--text-on-accent);
}
/* Bookmark ★ — same chip as the done toggle, accent-coloured when on.
   It renders before the done toggle, so it takes over the margin-left:auto
   right-alignment and hands the done toggle a fixed gap instead. */
.subject-page .lib-bookmark-toggle {
  margin-left: auto;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1.5px solid var(--card-border);
  background: var(--bg-secondary);
  color: var(--text-secondary);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
  transition: var(--transition);
}
.subject-page .lib-bookmark-toggle:hover {
  border-color: var(--accent-solid);
  color: var(--accent-solid);
}
.subject-page .lib-bookmark-toggle.is-bookmarked {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--text-on-accent);
}
.subject-page .lib-bookmark-toggle + .lib-done-toggle { margin-left: 8px; }
/* Share ⤴ — same chip again, and it comes FIRST of the three, so it takes
   the right-alignment and every chip after it gets the fixed gap. */
.subject-page .lib-share-toggle {
  margin-left: auto;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1.5px solid var(--card-border);
  background: var(--bg-secondary);
  color: var(--text-secondary);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
  transition: var(--transition);
}
.subject-page .lib-share-toggle:hover {
  border-color: var(--accent-solid);
  color: var(--accent-solid);
}
.subject-page .lib-share-toggle + .lib-bookmark-toggle,
.subject-page .lib-share-toggle + .lib-done-toggle,
.subject-page .lib-updated + .lib-share-toggle,
.subject-page .lib-exam-meta + .lib-share-toggle { margin-left: 8px; }
.subject-page .lib-sec-progress {
  margin-left: 10px;
  padding: 1px 8px;
  border-radius: 20px;
  font-size: 11px;
  font-weight: 700;
  color: var(--success-ink);
  background: var(--success-tint);
}
.subject-page .lib-sec-progress.all-done {
  color: var(--text-on-accent);
  background: var(--success);
}

/* Section view header (back link + heading + tags) */
.subject-page .lib-sec-head {
  padding: 0 2px 10px;
}
/* Unscoped: the same back link is used one level up on pages that don't
   wear .subject-page (the recorded-collection page). */
.lib-sec-back {
  display: inline-block;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--accent-1);
  text-decoration: none;
  margin-bottom: 14px;
  transition: var(--transition);
}
.lib-sec-back:hover { opacity: 0.75; }
/* Inside the card containers the container's own gap does the spacing, so
   the link drops its margin; on the section-card grid it also spans the
   full row so the cards start on the next line. */
.subject-page #topic-list > .lib-sec-back,
.subject-page #examBody > .lib-sec-back { margin-bottom: 0; }
.subject-page .lib-sec-grid > .lib-sec-back { grid-column: 1 / -1; }
.subject-page .lib-sec-head-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}
.subject-page .lib-sec-head-title {
  font-size: 1.5rem;
  font-weight: 800;
  color: var(--text-primary);
  margin: 0;
  letter-spacing: -0.01em;
}
.subject-page .lib-sec-head-desc {
  color: var(--text-secondary);
  font-size: 0.9rem;
  line-height: 1.6;
  margin: 8px 0 0;
}

@media (max-width: 600px) {
  .subject-page #topic-list.lib-sec-grid { grid-template-columns: 1fr; }
  .subject-page .lib-sec-card { padding: 18px 18px 14px; border-radius: 12px; }
}

/* ----- Notes hub: same card-style hero as the subject pages -----
   notes.html reuses .subject-breadcrumb + .subject-hero; these overrides
   just re-tune spacing so the first card row stays above the fold. */
.notes-hub .subject-hero-inner { max-width: 720px; }
.notes-hub .notes-search { margin-top: 8px; }
.notes-hub .notes-section { padding-top: 0; }
/* The search dropdown must escape the hero card, so the hub hero is
   overflow:visible and its blobs are clipped by .subject-hero-bg instead. */
.notes-hub .subject-hero { overflow: visible; }
.subject-hero-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}
/* Hub grid: fixed 3 columns so rows fill left-to-right as subjects grow;
   collapses to 2 / 1 columns on narrower screens. Shared by both hubs —
   the notes hub (.notes-hub) and the exams hub — so their subject cards
   measure identically; unscoped on purpose. */
.notes-subject-grid {
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  max-width: 1200px;
  padding-bottom: 80px;
}
@media (max-width: 1060px) {
  .notes-subject-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
  .notes-subject-grid { grid-template-columns: 1fr; }
}

/* ----- "Preparing for IIT JAM · Change exam" -----
   The bar above the subject grid once a reader has picked an exam. It is not
   rendered at all when the site has only one exam, so a single-exam site looks
   exactly as it did before the layer existed (subjects-render.js decides).
   Wraps rather than squashing, so a long exam name and the link stay legible
   on a phone. */
.exam-switch {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 12px;
  margin: 0 0 20px;
  padding: 12px 16px;
  /* Tinted, not card-coloured, and edged in the accent. The bar sits directly
     above a grid of cards, and while it WAS a card it read as one of them --
     a reader in the wrong exam scrolled straight past the way out. */
  border: 1px solid color-mix(in srgb, var(--accent-1) 35%, var(--card-border));
  border-left: 4px solid var(--accent-1);
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent-1) 9%, var(--card-bg));
  font-size: 14px;
}
.exam-switch-label { color: var(--text-secondary); }
.exam-switch-name {
  color: var(--text-primary);
  font-weight: 700;
  font-size: 16.5px;
}
/* A real button, not a text link. The link was the same weight as the label
   beside it, so nothing on the bar said "this one is pressable". */
.exam-switch-change {
  margin-left: auto;
  padding: 0 18px;
  border-radius: 50px;
  background: var(--accent-solid);
  border: 2px solid var(--accent-solid-border);
  /* Never #fff: azure and indigo dark put DARK ink on their accent. */
  color: var(--text-on-accent);
  font-weight: 600;
  font-size: 14px;
  /* A finger's worth of height, since this is the way out of the wrong exam. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  transition: var(--transition);
}
.exam-switch-change::after {
  content: "→";          /* the same arrow the subject cards end on */
  font-size: 15px;
}
.exam-switch-change:hover {
  text-decoration: none;
  filter: brightness(1.09);
}

/* Narrow: the button wraps onto its own line, where sitting half-width on the
   right reads like an afterthought. Give it the whole line instead. Width, not
   pointer type, is the right test here -- it is the wrap that looks wrong, and
   that happens on a dragged-narrow desktop window too. */
@media (max-width: 520px) {
  .exam-switch-change {
    margin-left: 0;
    width: 100%;
    justify-content: center;
  }
}

/* ----- Changing exam: the popup (subjects-render.js) -----
   The switch is a small decision taken mid-browse, so it happens over the
   page instead of on a screen of its own. Plain scrim, never a
   backdrop-filter: a full-viewport blur is recomputed every frame while the
   hero art animates behind it, and that is what made buttons feel dead
   (CLAUDE.md). Tokens only, so it follows every brand and both modes. */
/* ── Free / Premium switch (exam subject pages) ────────────────────
   One pill, two sides, a filled thumb that slides between them: Free on
   the left, Premium on the right. The thumb is a separate absolutely
   positioned span rather than a background on the selected button, so the
   movement is one transform and nothing reflows. The wrapper's data-tier
   drives it, which keeps the JS to a single attribute. */
.tier-switch {
  position: relative;
  display: inline-grid;
  grid-template-columns: 1fr 1fr;
  /* #topic-list is a flex column, which would stretch an inline-grid to the
     full width of the list. The pill stays its own size instead. */
  width: fit-content;
  align-self: flex-start;
  padding: 4px;
  margin: 0 0 20px;
  border: 1.5px solid var(--card-border);
  border-radius: 999px;
  background: var(--bg-secondary);
  box-shadow: inset 0 1px 3px var(--shadow);
}
.tier-switch-thumb {
  position: absolute;
  top: 4px; bottom: 4px; left: 4px;
  width: calc(50% - 4px);
  border-radius: 999px;
  background: var(--accent-solid);
  border: 1px solid var(--accent-solid-border);
  /* Same rule as every other gradient/filled pill on the site. */
  background-origin: border-box;
  box-shadow: 0 3px 12px var(--shadow);
  transition: transform 0.34s var(--ease-pop);
}
.tier-switch[data-tier="premium"] .tier-switch-thumb { transform: translateX(100%); }
.tier-switch-opt {
  position: relative;
  z-index: 1;
  min-height: 44px;
  padding: 0 22px;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.94rem;
  font-weight: 700;
  color: var(--text-secondary);
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.24s ease;
}
/* --text-on-accent, never #fff: the azure and indigo dark themes fill with
   a light accent and set dark ink here. */
.tier-switch-opt[aria-selected="true"] { color: var(--text-on-accent); }
.tier-switch-opt:hover[aria-selected="false"] { color: var(--text-primary); }
.tier-switch-opt:focus-visible { outline: 2px solid var(--accent-solid); outline-offset: 3px; }
.tier-switch-n { font-weight: 600; opacity: 0.75; margin-left: 5px; }

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

.expick-overlay {
  position: fixed; inset: 0; z-index: 3000;
  background: var(--scrim);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  overflow-y: auto;
  animation: accessFadeIn 0.18s ease;
}
.expick {
  width: 100%; max-width: 460px;
  /* --bg-secondary, not --card-bg: cards on this site are translucent, and a
     dialog you can read the page through is a dialog nobody can read. The
     same choice the exam page's own dialog makes (.xm-modal). */
  background: var(--bg-secondary);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-xl);
  padding: 24px;
  box-shadow: 0 24px 60px var(--shadow);
  animation: accessPopIn 0.22s var(--ease-pop);
}
.expick h3 {
  font-size: 1.2rem; font-weight: 800; margin-bottom: 6px;
  color: var(--text-primary);
}
.expick > p {
  font-size: 0.9rem; color: var(--text-secondary);
  line-height: 1.5; margin-bottom: 18px;
}
.expick-list { display: flex; flex-direction: column; gap: 10px; }

/* One row per exam: icon, name, what it is, and a tick on the one you are
   already in. A whole-row button, so the whole row is the target. */
.expick-item {
  display: flex; align-items: center; gap: 12px;
  width: 100%;
  min-height: 60px;
  padding: 12px 14px;
  text-align: left;
  border: 1.5px solid var(--card-border);
  border-radius: 14px;
  background: var(--bg-primary);
  color: var(--text-primary);
  cursor: pointer;
  transition: var(--transition);
}
.expick-item:hover {
  border-color: var(--accent-1);
  transform: translateY(-1px);
}
.expick-item.is-current {
  border-color: var(--accent-1);
  background: color-mix(in srgb, var(--accent-1) 12%, var(--bg-secondary));
}
.expick-icon {
  flex: 0 0 auto;
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  color: var(--accent-1);
}
/* The icon markup carries no stroke of its own — every place that draws one
   supplies it, so the icon takes the colour it sits in. */
.expick-icon svg { width: 22px; height: 22px; display: block; stroke: currentColor; }
.expick-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.expick-name { font-weight: 700; font-size: 15px; }
.expick-desc {
  font-size: 12.5px; color: var(--text-secondary); line-height: 1.4;
}
.expick-mark {
  margin-left: auto;
  color: var(--accent-1);
  font-weight: 800; font-size: 17px;
}
.expick-actions { display: flex; justify-content: flex-end; margin-top: 18px; }
.expick-cancel {
  min-height: 44px;
  padding: 0 20px;
  border-radius: 50px;
  border: 1px solid var(--card-border);
  background: var(--bg-secondary);
  color: var(--text-primary);
  font-size: 14px; font-weight: 600;
  cursor: pointer;
  transition: var(--transition);
}
.expick-cancel:hover { border-color: var(--accent-1); color: var(--accent-1); }

/* ----- Notes hub: subject cards (subjects-render.js) ----- */
.nsub-card {
  display: flex;
  flex-direction: column;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 18px;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  box-shadow: 0 2px 14px var(--shadow);
  transition: box-shadow var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-standard);
}
.nsub-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 38px var(--shadow);
  border-color: var(--accent-1);
}
/* The green band: icon + subject title live INSIDE it */
.nsub-head {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 18px 22px;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
}
.nsub-head h3 {
  color: var(--text-on-accent);
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.01em;
  margin: 0;
}
.nsub-icon {
  flex-shrink: 0;
  width: 42px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.18);
}
.nsub-icon svg {
  width: 26px;
  height: 26px;
  display: block;
  stroke: var(--text-on-accent);
}
/* Floating symbols in the band (owner pick 2026-07-17, "option A"): three
   faint icons drift right of the title, behind it (z-index), tinted by
   --text-on-accent so every brand/mode works. WHICH icons float is admin
   data (subjects.float_icons, falling back to the card's own icon), and
   the positions/sizes/rotations are randomized per load — both live in
   inline styles from subjects-render.js, so only the shared look is here.
   Band only — card bodies and section cards deliberately carry none. */
.nsub-head { position: relative; overflow: hidden; }
.nsub-head h3, .nsub-icon { position: relative; z-index: 2; }
.nsub-float {
  position: absolute;
  z-index: 1;
  opacity: 0.3;
  pointer-events: none;
  transform: rotate(var(--nf-rot, 0deg));
  animation: nsubFloat 10s ease-in-out infinite;
}
.nsub-float svg { display: block; width: 100%; height: 100%; stroke: var(--text-on-accent); }
/* Same resting-frame contract as the hero motifs: the keyframes carry the
   static rotation, so reduced-motion freezes elements onto it. */
@keyframes nsubFloat {
  0%, 100% { transform: translateY(0) rotate(var(--nf-rot, 0deg)); }
  50% { transform: translateY(-6px) rotate(calc(var(--nf-rot, 0deg) + 8deg)); }
}
.nsub-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px 22px 18px;
  flex: 1;
}
.nsub-body p {
  font-size: 14.5px;
  color: var(--text-secondary);
  line-height: 1.6;
  margin: 0;
  flex: 1;
}
.nsub-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
}
.nsub-progress {
  padding: 2px 9px;
  border-radius: 20px;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--success-ink);
  background: var(--success-tint);
}
.nsub-progress.all-done { color: var(--text-on-accent); background: var(--success); }
.nsub-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 12px;
  border-top: 1px solid var(--card-border);
  font-size: 14px;
  font-weight: 700;
  color: var(--accent-1);
}
.nsub-card:hover .lib-btn-arrow { transform: translateX(3px); }

/* ----- Classes hub: live class cards + class detail page (lcls-*) -----
   A class card IS a subject card (.nsub-card) with three additions: a
   state badge in the band, a when-line at the top of the body, and a lock
   chip. Everything else — band gradient, floating symbols, footer CTA —
   is inherited on purpose, so the two card families stay one design.
   Rendered by classes-render.js / class-page.js. */
.lcls-grid { padding-bottom: 0; }
/* "← All classes" on the recorded-collection page, lined up with the cards
   below it (.notes-subject-grid pads itself on top of the section's padding). */
.rec-back { max-width: 1200px; margin: 0 auto; padding: 0 24px; }

/* The badge is pinned to the band's top-right corner rather than sitting
   in the flex row: in flow, a two-line title shoved it around and made
   every band in the row a different height. The band reserves room for it
   with padding-right, and a min-height keeps all the cards' bands level
   whether the title wraps or not. */
.lcls-card .nsub-head {
  min-height: 82px;
  padding-right: 108px;
}
.lcls-badge {
  position: absolute;
  top: 14px;
  right: 16px;
  z-index: 3;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 11px;
  border-radius: 20px;
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
  background: rgba(255, 255, 255, 0.22);
  color: var(--text-on-accent);
}
.lcls-badge.live { background: var(--danger); color: #fff; }
.lcls-badge.ended { background: rgba(255, 255, 255, 0.16); opacity: 0.75; }
/* The pulsing dot is the one motion cue that says "happening right now". */
.lcls-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fff;
  animation: lclsPulse 1.8s ease-in-out infinite;
}
@keyframes lclsPulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.45; transform: scale(0.72); }
}
.lcls-when {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--accent-1);
}
.lcls-lock {
  padding: 2px 9px;
  border-radius: 20px;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--danger-ink);
  background: var(--danger-tint);
  white-space: nowrap;
}
/* A live card gets a warm ring so it reads as the urgent one in the row. */
.lcls-card[data-live-state="live"] {
  border-color: var(--danger);
  box-shadow: 0 2px 14px var(--shadow), 0 0 0 1px var(--danger);
}
.lcls-card[data-live-state="ended"] .nsub-head { filter: saturate(0.55); }

/* ----- Class detail page (class.html) ----- */
/* On the class page the badge is a normal block above the title, not the
   card's pinned corner chip — so it opts back out of the absolute rule. */
.lcls-badge--hero {
  position: static;
  margin-bottom: 12px;
  padding: 5px 13px;
  font-size: 12px;
  background: var(--accent-1);
  color: var(--text-on-accent);
}
.lcls-badge--hero:empty { display: none; }
.lcls-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 18px;
  margin-top: 12px;
  font-size: 15px;
  font-weight: 600;
  color: var(--text-secondary);
}
/* Detail body beside a sticky join panel. The panel is FIRST in the DOM so
   the stacked mobile order puts the join link at the top — the same
   source-order trick .xm-result-cols uses for the leaderboard. */
/* .subject-page-wrap deliberately carries no max-width or side padding —
   each section sizes itself (see the note on that rule). This is this
   page's container, the counterpart of #topic-list on subject pages. */
.lcls-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: 28px;
  align-items: start;
  max-width: 1100px;
  margin: 28px auto 0;
  padding: 0 16px 60px;
}
/* "← All classes", lined up with the columns above (the page wrap has no
   width of its own, so the link needs the same box as .lcls-cols). */
.lcls-back { max-width: 1100px; margin: 0 auto; padding: 0 16px; }
.lcls-back .lib-sec-back { margin-bottom: 0; }
.lcls-side { grid-column: 2; position: sticky; top: 106px; }
.lcls-main { grid-column: 1; grid-row: 1; }
@media (max-width: 900px) {
  .lcls-cols { grid-template-columns: 1fr; }
  .lcls-side { grid-column: 1; position: static; }
  .lcls-main { grid-column: 1; grid-row: auto; }
}
.lcls-h2 {
  font-size: 20px;
  font-weight: 800;
  margin: 0 0 10px;
}
.lcls-h2 + .lcls-para, .lcls-h2 + .lcls-topics { margin-bottom: 26px; }
.lcls-para {
  font-size: 15.5px;
  line-height: 1.7;
  color: var(--text-secondary);
  margin: 0;
}
.lcls-topics {
  /* Opts back in: the global reset is `ul { list-style: none }`, and these
     genuinely are bullets. */
  list-style: disc;
  margin: 0;
  padding-left: 22px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--text-secondary);
}
.lcls-topics li::marker { color: var(--accent-1); }
.lcls-join {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 18px;
  padding: 22px;
  box-shadow: 0 2px 14px var(--shadow);
}
.lcls-join.locked { border-color: var(--danger-border); }
.lcls-join-title {
  font-size: 17px;
  font-weight: 800;
  margin: 0 0 8px;
}
.lcls-join-note {
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-secondary);
  margin: 0 0 14px;
}
.lcls-join-btn { display: block; text-align: center; width: 100%; }
.lcls-join-btn--live {
  background: var(--danger);
  border-color: var(--danger);
}

/* ----- Shared loading skeletons + error-with-retry states ----- */
@keyframes skelShimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.skel-line {
  height: 12px;
  border-radius: 6px;
  /* Tint from --text-secondary, not --card-border: the light themes'
     card-border is near-white (rgba 255/.85), which disappears against a
     card surface. text-secondary is legible on cards in every brand and
     mode by definition, so a low-alpha mix of it always reads. */
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--text-secondary) 16%, transparent) 25%,
    color-mix(in srgb, var(--text-secondary) 6%, transparent) 45%,
    color-mix(in srgb, var(--text-secondary) 16%, transparent) 65%);
  background-size: 200% 100%;
  animation: skelShimmer 1.4s ease-in-out infinite;
}
/* Hub subject-card skeleton (mirrors .nsub-card: band + body) */
.nsub-skel-card {
  border: 1px solid var(--card-border);
  border-radius: 18px;
  overflow: hidden;
  background: var(--card-bg);
}
.nsub-skel-head {
  height: 78px;
  /* same --text-secondary tint as .skel-line, and for the same reason */
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--text-secondary) 16%, transparent) 25%,
    color-mix(in srgb, var(--text-secondary) 6%, transparent) 45%,
    color-mix(in srgb, var(--text-secondary) 16%, transparent) 65%);
  background-size: 200% 100%;
  animation: skelShimmer 1.4s ease-in-out infinite;
  opacity: 0.6;
}
.nsub-skel-body { display: flex; flex-direction: column; gap: 13px; padding: 18px 22px 22px; }
/* Subject-page note-card skeleton (mirrors .lib-topic-card) */
.subject-page .lib-skel-card {
  border: 1px solid var(--card-border);
  border-radius: 16px;
  background: var(--card-bg);
  padding: 20px 22px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* Dashboard skeletons (stat values + the continue/recent boxes) — swapped
   for real values, a <template> empty state, or an error line once
   /api/progress/dashboard resolves (see dashboard.html's loader). */
.dash-skel-stat { display: inline-block; width: 48px; height: 30px; border-radius: 8px; margin: 5px 0; }
.dash-skel-row { height: 13px; margin: 18px 0; }
.dash-skel-row.short { width: 60%; }
.lib-empty, .lib-error { grid-column: 1 / -1; }
.lib-error {
  text-align: center;
  padding: 40px 0;
  color: var(--text-secondary);
  font-size: 0.95rem;
}
.lib-error p { margin: 0 0 14px; }
.lib-retry-btn {
  padding: 9px 22px;
  border-radius: 24px;
  border: 1px solid var(--accent-1);
  color: var(--accent-1);
  background: transparent;
  font-weight: 700;
  font-size: 0.9rem;
  cursor: pointer;
  font-family: inherit;
  transition: var(--transition);
}
.lib-retry-btn:hover { background: var(--accent-1); color: var(--text-on-accent); }

/* ----- "Continue reading" strip (notes hub, logged-in only) -----
   Kept to ONE slim row of chips (label + up to 3 pills) so it never
   pushes the first row of subject cards below the fold. */
.cont-strip { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.cont-strip:empty { display: none; }
.cont-strip-inner {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 10px 12px;
  margin-bottom: 22px;
}
.cont-strip-label {
  flex-shrink: 0;
  font-size: 13.5px;
  font-weight: 800;
  color: var(--text-primary);
  letter-spacing: 0.01em;
}
.cont-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  max-width: 300px;
  padding: 8px 14px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 24px;
  text-decoration: none;
  box-shadow: 0 2px 10px var(--shadow);
  transition: box-shadow var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-standard), border-color var(--dur-base) var(--ease-standard);
}
.cont-chip:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 20px var(--shadow);
  border-color: var(--accent-1);
}
.cont-chip-title {
  font-weight: 700;
  font-size: 13.5px;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cont-chip-page {
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 700;
  color: var(--accent-1);
  white-space: nowrap;
}
.cont-chip:hover .lib-btn-arrow { transform: translateX(3px); }
/* When the strip renders (subjects-render.js adds has-cont-strip to <body>),
   shave a little top/hero spacing so the strip's height is absorbed and the
   first card row still fits above the fold at 1366×768. The wrap padding
   never dips below the ~88px navbar, so nothing hides behind the header. */
.notes-hub.has-cont-strip .subject-page-wrap { padding-top: 98px; }
.notes-hub.has-cont-strip .subject-hero { min-height: 240px; margin-bottom: 20px; }
/* The exams hub uses the same strip to FILL the hero→cards gap rather than add
   to it, so its section's top padding shrinks by about the strip's height when
   the strip renders. Without this the chips would just push the cards down and
   the gap would grow. The exams hub carries no .notes-hub class, so the two
   hubs' adjustments cannot collide. */
body.has-cont-strip #exams { padding-top: 40px; }

/* ----- Note card extras: "New" badge + updated date ----- */
.subject-page .lib-new-badge {
  flex-shrink: 0;
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-on-accent);
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  padding: 2px 8px;
  border-radius: 12px;
}
.subject-page .lib-updated {
  margin-left: auto;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
}
/* When the updated date is present it owns the auto margin; the done
   toggle then sits right next to it. */
.subject-page .lib-updated + .lib-done-toggle { margin-left: 10px; }

/* ----- Note filter pills (subject pages) ----- */
/* The notes subject page's "← All subjects" link, sitting above the filter
   bar. Same box as the bar and the card list so all three line up. */
.subject-page .lib-hub-back {
  max-width: 820px;
  margin: 0 auto;
  padding: 0 16px 14px;
}
.subject-page .lib-hub-back .lib-sec-back { margin-bottom: 0; }
.subject-page .lib-filter-bar {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  max-width: 820px;
  margin: 0 auto;
  padding: 0 16px 18px;
}
.subject-page .lib-filter-pill {
  padding: 6px 16px;
  border-radius: 20px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text-secondary);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
  transition: var(--transition);
}
.subject-page .lib-filter-pill:hover { border-color: var(--accent-1); color: var(--accent-1); }
.subject-page .lib-filter-pill.active {
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  border-color: transparent;
  color: var(--text-on-accent);
  background-origin: border-box;   /* see .btn — stops the end-colour rim */
}

@media (max-width: 640px) {
  .notes-hub .page-hero { padding: 92px 20px 18px; }
  .notes-hub .page-hero h1 { font-size: 28px; }
  .nsub-head { padding: 15px 18px; }
  .nsub-body { padding: 14px 18px 16px; }
}

/* ----- Notes hub search (search.js) ----- */
.notes-search {
  position: relative;
  max-width: 520px;
  margin: 26px auto 0;
  text-align: left;
}
.notes-search input[type="search"] {
  width: 100%;
  padding: 13px 20px;
  border-radius: 14px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text-primary);
  font-size: 15px;
  font-family: inherit;
  box-shadow: 0 4px 18px var(--shadow);
  transition: var(--transition);
}
.notes-search input[type="search"]:focus {
  outline: none;
  border-color: var(--accent-1);
  box-shadow: 0 6px 24px var(--shadow);
}
.notes-search-results {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  z-index: 100;
  background: var(--bg-secondary);
  border: 1px solid var(--card-border);
  border-radius: 14px;
  box-shadow: 0 18px 48px var(--shadow);
  overflow: hidden;
  max-height: 380px;
  overflow-y: auto;
}
.search-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 16px;
  text-decoration: none;
  color: inherit;
  border-bottom: 1px solid var(--card-border);
  transition: var(--transition);
}
.search-row:last-child { border-bottom: none; }
.search-row:hover { background: var(--card-bg); }
.search-kind {
  flex-shrink: 0;
  margin-top: 2px;
  padding: 2px 8px;
  border-radius: 20px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: var(--info-tint);
  color: var(--accent-1);
}
.search-kind.note {
  background: var(--success-tint);
  color: var(--success-ink);
}
.search-main { min-width: 0; }
.search-title { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.search-sub {
  font-size: 12.5px;
  color: var(--text-secondary);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-empty { padding: 18px; font-size: 14px; color: var(--text-secondary); text-align: center; }
/* Keyboard-highlighted row (↑/↓ in search.js) */
.search-row.active { background: var(--card-bg); box-shadow: inset 3px 0 0 var(--accent-1); }
/* Query match inside a result title */
.search-mark {
  background: var(--info-mark);
  color: inherit;
  border-radius: 3px;
  padding: 0 1px;
}
/* Recent searches (shown on focus while the box is empty) */
.search-recent-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 16px 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.search-recent-clear {
  border: none;
  background: none;
  color: var(--accent-1);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  font-family: inherit;
  padding: 2px 4px;
}
.search-recent-clear:hover { text-decoration: underline; }
.search-row-recent {
  width: 100%;
  border: none;
  border-bottom: 1px solid var(--card-border);
  background: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

/* ----- Toasts (window.showToast in script.js) ----- */
.toast-box {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 5000;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  pointer-events: none;
}
.toast {
  background: var(--card-bg);
  color: var(--text-primary);
  border: 1px solid var(--card-border);
  border-left: 4px solid var(--accent-1);
  border-radius: 12px;
  padding: 11px 18px;
  font-size: 14px;
  font-weight: 500;
  box-shadow: 0 12px 32px var(--shadow);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-standard);
  max-width: min(92vw, 460px);
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast-success { border-left-color: var(--success); }
.toast-error { border-left-color: var(--danger); }

/* ----- "You have a reply" nudge (script.js, any page) -----
   Told once per browser session that a correction they sent has been
   answered. Unlike a toast it does not time out — it waits to be read or
   closed — so it sits in the corner, out of the toast column. */
.reply-nudge {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 4900;                /* under toasts (5000), over the page */
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: min(92vw, 420px);
  padding: 12px 14px;
  border-radius: 14px;
  border: 1px solid var(--card-border);
  border-left: 4px solid var(--accent-1);
  background: var(--card-bg);
  box-shadow: 0 12px 32px var(--shadow);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-base) var(--ease-standard),
              transform var(--dur-base) var(--ease-standard);
}
.reply-nudge.show { opacity: 1; transform: translateY(0); }
.reply-nudge-icon {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  /* Ink from the theme, never #fff: on azure's moonlit blue white reads 2.69
     and fails AA — which is why every theme ships --text-on-accent. */
  background: var(--accent-1); color: var(--text-on-accent);
  font-size: 16px; font-weight: 700;
}
.reply-nudge-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.reply-nudge-text strong { font-size: 14px; color: var(--text-primary); }
.reply-nudge-text span { font-size: 12.5px; color: var(--text-secondary); }
.reply-nudge-go {
  flex: none; min-height: 44px; display: inline-flex; align-items: center;
  padding: 0 14px; border-radius: 10px;
  background: var(--accent-1); color: var(--text-on-accent);
  font-size: 13.5px; font-weight: 700; text-decoration: none;
}
.reply-nudge-go:hover { filter: brightness(1.06); }
.reply-nudge-close {
  flex: none; width: 44px; height: 44px; padding: 0;
  border: none; background: none; cursor: pointer;
  color: var(--text-secondary); font-size: 22px; line-height: 1;
}
.reply-nudge-close:hover { color: var(--text-primary); }
/* A narrow window has no room for a corner card — span the width instead.
   Width, not pointer type: this is layout, not a touch fix. */
@media (max-width: 520px) {
  .reply-nudge { right: 12px; left: 12px; bottom: 12px; max-width: none; }
}

/* ----- Command palette (Ctrl+K / Cmd+K, built on demand by script.js) ----- */
/* Plain scrim on purpose: a full-viewport backdrop-filter is recomputed every
   frame while the hero motifs animate behind it and stalls weak GPUs. */
.cmdk-overlay {
  position: fixed;
  inset: 0;
  z-index: 4000;               /* above page modals (3000), below toasts (5000) */
  display: none;
  background: var(--scrim);
  padding: 12vh 20px 20px;
}
.cmdk-overlay.open { display: flex; justify-content: center; align-items: flex-start; }
.cmdk-panel {
  width: min(560px, 100%);
  max-height: 62vh;
  display: flex;
  flex-direction: column;
  background: var(--bg-secondary);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-3);
  overflow: hidden;
}
.cmdk-input {
  border: none;
  outline: none;
  background: transparent;
  color: var(--text-primary);
  font: inherit;
  font-size: 16px;
  padding: 15px 18px;
  border-bottom: 1px solid var(--card-border);
}
.cmdk-list {
  flex: 1;
  overflow-y: auto;
  padding: 8px;
}
.cmdk-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 12px;
  border-radius: var(--radius-md);
  text-decoration: none;
  color: var(--text-primary);
  transition: background var(--dur-fast) var(--ease-standard);
}
.cmdk-row:hover, .cmdk-row.active { background: var(--input-bg); }
.cmdk-kind {
  flex-shrink: 0;
  min-width: 58px;
  text-align: center;
  font-size: 11px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: var(--radius-full);
  border: 1px solid var(--card-border);
  color: var(--text-secondary);
  background: var(--input-bg);
}
.cmdk-kind.note { color: var(--accent-solid); }
.cmdk-main { min-width: 0; }
.cmdk-title { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmdk-sub { font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmdk-empty { padding: 26px 12px; text-align: center; color: var(--text-secondary); font-size: 14px; }
.cmdk-hint {
  padding: 8px 18px;
  font-size: 11.5px;
  color: var(--text-secondary);
  border-top: 1px solid var(--card-border);
}

/* ----- Subject hero ----- */
/* Kept deliberately compact (paired with the icon/title trims below) so
   the first two-three note cards sit above the fold at 1366×768. */
.subject-hero {
  position: relative;
  overflow: hidden;
  text-align: center;
  padding: 34px 24px 38px;
  margin-bottom: 28px;
  border-radius: 24px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
}
.subject-hero .hero-blob {
  opacity: 0.28;
}
/* Share button pinned to the hero card's top-right corner (the breadcrumb
   owns the top-left). script.js wires any [data-share]: native share sheet
   on coarse-pointer devices, copy-link + toast elsewhere. */
.subject-hero .subject-share-btn {
  position: absolute;
  top: 14px;
  right: 16px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  border-radius: var(--radius-full);
  border: 1px solid var(--card-border);
  background: var(--input-bg);
  color: var(--text-secondary);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: var(--transition);
}
.subject-hero .subject-share-btn:hover {
  color: var(--accent-solid);
  border-color: var(--accent-solid);
}
.subject-hero-inner {
  position: relative;
  z-index: 2;
  max-width: 640px;
  margin: 0 auto;
}
.subject-hero h1 {
  font-size: 36px;
  font-weight: 800;
  line-height: 1.15;
  margin-bottom: 10px;
  text-transform: none;
  letter-spacing: -0.02em;
}
.subject-hero-desc {
  font-size: 16px;
  color: var(--text-secondary);
  line-height: 1.6;
  margin-bottom: 16px;
}
.subject-topic-count {
  display: inline-block;
  padding: 6px 16px;
  border-radius: 50px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--input-bg);
  border: 1px solid var(--card-border);
}

/* ----- Subject topic cards ----- */
.subject-topics {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.subject-topic-card {
  display: flex;
  align-items: center;
  gap: 28px;
  width: 100%;
  padding: 28px 32px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 20px;
  text-decoration: none;
  color: inherit;
  transition: var(--transition);
}
.subject-topic-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 16px 40px var(--shadow);
}
.subject-topic-num {
  flex-shrink: 0;
  width: 72px;
  font-size: 48px;
  font-weight: 800;
  line-height: 1;
  text-align: center;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  opacity: 0.85;
}
.subject-topic-content {
  flex: 1;
  min-width: 0;
  text-align: left;
}
.subject-topic-title {
  font-size: 22px;
  font-weight: 700;
  margin-bottom: 8px;
  line-height: 1.3;
}
.subject-topic-desc {
  font-size: 15px;
  color: var(--text-secondary);
  line-height: 1.6;
}
.subject-topic-arrow {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  font-size: 20px;
  color: var(--accent-1);
  background: var(--input-bg);
  border: 1px solid var(--card-border);
  opacity: 0.65;
  transition: var(--transition);
}
.subject-topic-card:hover .subject-topic-arrow {
  transform: translateX(4px);
  opacity: 1;
}

/* ----- Subject page responsive ----- */
@media (max-width: 768px) {
  .subject-hero h1 { font-size: 30px; }
  .subject-hero { padding: 30px 20px 34px; margin-bottom: 24px; }
  .subject-topic-card { gap: 20px; padding: 24px 22px; }
  .subject-topic-num { width: 56px; font-size: 38px; }
  .subject-topic-title { font-size: 19px; }
}
@media (max-width: 600px) {
  /* Extra hero top padding so the in-card breadcrumb clears the content
     when the card gets narrow. */
  .subject-hero { padding-top: 52px; }
  .subject-hero h1 { font-size: 28px; }
  .subject-topic-card {
    align-items: flex-start;
    gap: 16px;
    padding: 22px 20px;
  }
  .subject-topic-num {
    width: 48px;
    font-size: 32px;
  }
  .subject-topic-arrow {
    position: absolute;
    top: 22px;
    right: 20px;
    width: 36px;
    height: 36px;
    font-size: 16px;
  }
  .subject-topic-content {
    padding-right: 44px;
  }
}

/* ===== RESPONSIVE ===== */
@media (max-width: 600px) {
  .topic-card-horizontal { flex-direction: column; align-items: flex-start; text-align: left; }
  .notes-subject-card { padding: 24px 16px; }
}

/* ============================================================
   ILLUSTRATED HOMEPAGE SCENES
   The homepage is one continuous painted landscape built from
   five panels (frontend/images/panel-*.webp). Each .scene shows
   its day panel on ::before and its night panel on ::after;
   toggling [data-mode] crossfades the two layers (opacity is
   animatable, background-image is not).
   Seam gradients are color-matched to the panels' sampled edge
   pixels so adjacent scenes read as one unbroken illustration
   (they also cover the generator watermark near panel edges).
   Scoped to .scene — no other page is affected.
   ============================================================ */

.scene {
  position: relative;
  isolation: isolate;
  display: flow-root; /* contain child margins — they must not escape and
                         open body-colored gaps between stacked scenes */
}
.scene::before,
.scene::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-repeat: no-repeat, no-repeat;
  background-size: 100% 100%, cover;
  background-position: center, var(--scene-pos, center);
  transition: opacity 0.9s ease;
}
.scene::before {
  background-image: var(--scene-grad-day), var(--scene-day);
  opacity: 1;
}
.scene::after { opacity: 0; }
/* Night images only start downloading once .night-ready is set on <html>
   (added on first dark-mode use, or idle-preloaded after page load). */
.night-ready .scene::after,
[data-mode="dark"] .scene::after {
  background-image: var(--scene-grad-night), var(--scene-night);
}
[data-mode="dark"] .scene::before { opacity: 0; }
[data-mode="dark"] .scene::after { opacity: 1; }

/* --- Panel assignment + edge-matched seam gradients (v2 day set) ---
   Day art: one unique panel per scene, no reuse. Panel 2 (spare clearing)
   was retired when the features section was cut; the interlude/CTA panels
   are deliberately swapped (village = art-only showcase, glade = CTA rest).
   Night art: still the v1 *-night files until the v2 night set is painted;
   night gradients therefore keep their v1 tunings below.
   Day joints (midpoint of adjacent 24px edge strips):
   hero↓#c2c6c0  / notes↑#f6f4fa  → #dcdddd  (mist)
   notes↓#8fa97e / classes↑#69a947 → #7ca962  (grass)
   classes↓#76976e / exams↑#5b9b4b → #68995d  (grass)
   exams↓#6f8e6f / village↑#8fb38b → #7fa17d  (misty grass)
   village↓#aeb7a3 / glade↑#8eb38b → #9eb597  (misty grass)
   glade = final panel; its gradient ends in the footer shade band  */

.scene-hero {
  --scene-day: url("images/p1-day.webp");
  --scene-night: url("images/panel-1-hero-night.webp");
  --scene-grad-day: linear-gradient(to bottom, rgba(0,0,0,0) 90%, #dcdddd 100%);
  --scene-grad-night: linear-gradient(to bottom, rgba(0,0,0,0) 90%, #93aabd 100%);
}
.scene-hero .hero-scroll { bottom: calc(16vh + 24px); }
.scene-notes {
  --scene-day: url("images/p3-day.webp");
  --scene-night: url("images/panel-3-notes-night.webp");
  --scene-grad-day: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 93%, #7ca962 100%);
  --scene-grad-night: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 93%, #244a38 100%);
  /* Slide up under the hero so the two painted mist bands merge into one
     continuous fog line instead of stacking into a tall empty gap. */
  margin-top: -16vh;
}
/* Feather the overlapped top edge so the academy's mist dissolves into the
   hero valley instead of cutting a straight line across it. */
.scene-notes::before,
.scene-notes::after {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 14%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 14%);
}
.scene-classes {
  --scene-day: url("images/p4-day.webp");
  --scene-night: url("images/panel-3-notes-night.webp");
  --scene-grad-day: linear-gradient(to bottom, #7ca962 0%, rgba(0,0,0,0) 7%, rgba(0,0,0,0) 93%, #68995d 100%);
  --scene-grad-night: linear-gradient(to bottom, #244a38 0%, rgba(0,0,0,0) 7%, rgba(0,0,0,0) 93%, #254b43 100%);
}
.scene-exams {
  --scene-day: url("images/p5-day.webp");
  --scene-night: url("images/panel-4-exams-night.webp");
  --scene-grad-day: linear-gradient(to bottom, #68995d 0%, rgba(0,0,0,0) 7%, rgba(0,0,0,0) 93%, #7fa17d 100%);
  --scene-grad-night: linear-gradient(to bottom, #254b43 0%, rgba(0,0,0,0) 7%, rgba(0,0,0,0) 93%, #284e3a 100%);
}
/* Interlude = the VILLAGE, shown big as a pure art moment (no content). */
.scene-interlude {
  height: 52vw;
  --scene-day: url("images/p7-day.webp");
  --scene-night: url("images/panel-4-exams-night.webp");
  --scene-grad-day: linear-gradient(to bottom, #7fa17d 0%, rgba(0,0,0,0) 8%, rgba(0,0,0,0) 92%, #9eb597 100%);
  --scene-grad-night: linear-gradient(to bottom, #284e3a 0%, rgba(0,0,0,0) 8%, rgba(0,0,0,0) 92%, #2e5448 100%);
}
/* CTA + FOOTER = the quiet GLADE, the journey's end. The banner rests on
   the calm brook clearing; the footer sits on a deep-green shade band that
   the gradient paints over the glade's bottom edge (doubles as scrim). */
.scene-cta {
  --scene-day: url("images/p6-day.webp");
  --scene-night: url("images/panel-5-footer-night.webp");
  --scene-grad-day: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(14,34,22,0) 55%, rgba(14,34,22,0.7) 78%, #0e2214 100%);
  --scene-grad-night: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(13,43,40,0) 55%, rgba(13,43,40,0.9) 80%, #0d2b28 100%);
  /* Overlap the village so its misty foreground merges with the glade —
     same feathered-merge as the hero→notes join. */
  margin-top: -10vh;
}
.scene-cta::before,
.scene-cta::after {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 12%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 12%);
}
/* Footer brand must be light over the shade band — undo the green
   gradient-clip the navbar logo uses. */
.scene .footer .logo span {
  background: none;
  -webkit-background-clip: initial;
  background-clip: initial;
  color: #eef4ec;
}
.scene .footer .logo .logo-highlight { color: #9fd3a6; }

/* --- Room to breathe: on desktop each content scene shows (nearly) its
       full 16:9 panel instead of a cramped crop, content centered --- */
@media (min-width: 1150px) {
  .scene-notes, .scene-classes, .scene-exams {
    min-height: 50vw;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
  /* CTA scene hosts the banner (centered in the open glade) AND the
     footer (resting on the shade band at the bottom). */
  .scene-cta {
    min-height: 54vw;
    display: flex;
    flex-direction: column;
  }
  .scene-cta .cta-wrap { margin: auto 0; }
}
@media (max-width: 1149px) {
  .scene-interlude { height: 45vh; }
}

/* --- Sections go transparent inside scenes (the panels are the bg) --- */
.scene .features-section,
.scene .classes-section,
.scene .footer {
  background: transparent;
}

/* --- Text readability over the artwork --- */
.scene .hero h1 {
  text-shadow: 0 2px 18px rgba(255, 255, 255, 0.65), 0 1px 4px rgba(255, 255, 255, 0.4);
}
.scene .section-header h2,
.scene .cta-banner h2 {
  background-image: linear-gradient(90deg, #2e7a40 0%, #4f9c5b 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  text-shadow: none;
  filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.95))
          drop-shadow(0 0 26px rgba(255, 255, 255, 0.7));
}
[data-mode="dark"] .scene .section-header h2,
[data-mode="dark"] .scene .cta-banner h2 {
  background-image: linear-gradient(90deg, #b5dfae 0%, #cfe8b8 100%);
  filter: drop-shadow(0 2px 10px rgba(6, 12, 24, 0.75)) drop-shadow(0 0 16px rgba(183, 226, 177, 0.28));
}
.scene .hero p,
.scene .section-header p {
  color: var(--text-primary);
  opacity: 0.92;
  text-shadow: 0 0 8px rgba(255, 255, 255, 0.9), 0 0 22px rgba(255, 255, 255, 0.65);
}
[data-mode="dark"] .scene .hero h1 {
  text-shadow: 0 2px 18px rgba(6, 12, 24, 0.85), 0 1px 4px rgba(6, 12, 24, 0.6);
}
[data-mode="dark"] .scene .hero p,
[data-mode="dark"] .scene .section-header p {
  text-shadow: 0 1px 12px rgba(6, 12, 24, 0.8);
}
.scene .hero-scroll {
  color: var(--text-primary);
  opacity: 0.75;
  text-shadow: 0 1px 8px rgba(255, 255, 255, 0.5);
}
[data-mode="dark"] .scene .hero-scroll { text-shadow: 0 1px 8px rgba(6, 12, 24, 0.7); }

/* Headline gradient tuned per theme: the default accent gradient washes
   out against the bright day sky and disappears into the night sky.
   text-shadow must be none here — with background-clip:text the inherited
   h1 shadow paints over the gradient glyphs; drop-shadow goes outside. */
.scene .hero-highlight {
  background-image: linear-gradient(90deg, #2e7a40 0%, #4f9c5b 100%);
  text-shadow: none;
  filter: drop-shadow(0 2px 10px rgba(255, 255, 255, 0.5));
}
[data-mode="dark"] .scene .hero-highlight {
  background-image: linear-gradient(90deg, #b5dfae 0%, #cfe8b8 100%);
  filter: drop-shadow(0 2px 10px rgba(6, 12, 24, 0.75)) drop-shadow(0 0 18px rgba(183, 226, 177, 0.3));
}

/* Soft halo behind section headings so they hold up over busy artwork
   (bridge, river, sunlit slopes) without boxing them in. */
.scene .section-header {
  padding: 18px 28px;
  border-radius: 24px;
  background: radial-gradient(ellipse closest-side, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
}
[data-mode="dark"] .scene .section-header {
  background: radial-gradient(ellipse closest-side, rgba(8, 16, 26, 0.55), rgba(8, 16, 26, 0));
}

/* Outline buttons need a frosted backing to stay visible over the panels */
.scene .btn-outline {
  background: rgba(255, 255, 255, 0.5);
  backdrop-filter: blur(8px);
}
.scene .btn-outline:hover { background: var(--accent-1); }
[data-mode="dark"] .scene .btn-outline {
  background: rgba(10, 20, 28, 0.55);
}
[data-mode="dark"] .scene .btn-outline:hover { background: var(--accent-1); }

/* Math symbols float over the illustrated sky */
.scene .hero-symbol { opacity: 0.3; color: #2c4423; }
[data-mode="dark"] .scene .hero-symbol { color: #ece9d4; opacity: 0.22; }

/* --- Frosted cards: solid enough to read, clear enough to see the painting --- */
.scene .feature-card,
.scene .note-card,
.scene .class-card,
.scene .coming-soon-message {
  background: rgba(255, 252, 244, 0.72);
  border-color: rgba(255, 255, 255, 0.85);
}
[data-mode="dark"] .scene .feature-card,
[data-mode="dark"] .scene .note-card,
[data-mode="dark"] .scene .class-card,
[data-mode="dark"] .scene .coming-soon-message {
  background: rgba(8, 18, 12, 0.62);
  border-color: rgba(255, 255, 255, 0.16);
}

/* Coming-soon card (was inline-styled; now shares the frosted treatment) */
.coming-soon-message {
  text-align: center;
  padding: 48px 40px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px);
  border-radius: 24px;
  box-shadow: 0 6px 20px var(--shadow);
}
.coming-soon-emoji { font-size: 32px; display: block; margin-bottom: 12px; }
.coming-soon-message p { color: var(--text-secondary); margin-top: 8px; font-size: 15px; }

/* --- Final CTA: whisper of frost — the village art is the star, the
       heading halo + solid button carry the readability --- */
.scene .cta-wrap { padding: 0 24px; }
.scene .cta-banner {
  background: rgba(255, 252, 244, 0.22);
  backdrop-filter: blur(3px);
  border: 1px solid rgba(255, 255, 255, 0.45);
  box-shadow: none;
  color: var(--text-primary);
}
.scene .cta-banner p {
  text-shadow: 0 0 8px rgba(255, 255, 255, 0.9), 0 0 22px rgba(255, 255, 255, 0.65);
}
.scene .cta-banner p { opacity: 0.85; }
[data-mode="dark"] .scene .cta-banner {
  background: rgba(8, 18, 12, 0.4);
  border-color: rgba(255, 255, 255, 0.14);
}

/* Outline buttons float on bright painted grass — frost them so they read
   as interactive instead of ghosted. */
.scene .btn-outline {
  background: rgba(255, 252, 244, 0.78);
  backdrop-filter: blur(10px);
}
.scene .btn-outline:hover { background: var(--accent-1); }
[data-mode="dark"] .scene .btn-outline {
  background: rgba(8, 18, 12, 0.6);
}
[data-mode="dark"] .scene .btn-outline:hover { background: var(--accent-1); }

/* --- Footer: light-on-dark in BOTH themes (it sits on the painted
       shadow band at the bottom of panel 5) --- */
.scene .footer { border-top: none; padding-top: 80px; }
.scene .footer .logo-text,
.scene .footer-links-title,
.scene .footer-social-title { color: #eef4ec; }
.scene .footer-brand p,
.scene .footer-links a,
.scene .footer-social a,
.scene .footer-bottom { color: rgba(238, 244, 236, 0.75); }
.scene .footer-links a:hover,
.scene .footer-social a:hover { color: #ffffff; }
.scene .footer-bottom { border-top-color: rgba(238, 244, 236, 0.2); }

/* --- Section rhythm inside scenes (tightened for the 8-panel layout:
       every section must fit inside one 16:9 panel at ~1.0x zoom) --- */
.scene .section-cta { padding-bottom: 12px; margin-top: 32px; }
.scene .features-section,
.scene .classes-section { padding: 64px 24px 56px; }
.scene .section-header { margin-bottom: 34px; }
.scene-cta .cta-wrap { padding-top: 90px; }
.scene-cta .footer { padding-top: 30px; }

/* --- Featured preview grid (notes / classes / exams): up to 4 slots, but
       only the cards that exist render — rows stay centered at any count --- */
.preview-grid-4 {
  max-width: 1240px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 285px));
  justify-content: center;
  gap: 20px;
  padding: 0 12px;
}
/* Empty state: a single wide, centered info card (nothing featured yet) */
.preview-grid-4.uc-single { display: flex; justify-content: center; }
.preview-grid-4.uc-single .ph-card { width: 100%; max-width: 620px; }
.scene .features-grid {
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
  max-width: 1240px;
}
@media (max-width: 1150px) {
  .scene .features-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
  .preview-grid-4 { grid-template-columns: 1fr; }
  .scene .features-grid { grid-template-columns: 1fr; }
}

/* compact cards so a row of 4 stays inside the panel height */
.scene .feature-card, .scene .note-card { padding: 26px 22px; }
.scene .feature-card h3, .scene .note-card h3 { font-size: 16.5px; }
.scene .feature-card p, .scene .note-card p { font-size: 13px; }

/* --- Placeholder slots: reserved, clearly intentional --- */
.ph-card {
  padding: 26px 22px;
  border-radius: 20px;
  border: 2px dashed var(--card-border);
  background: var(--card-bg);
  backdrop-filter: blur(12px);
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 200px;
  opacity: 0.82;
  transition: var(--transition);
}
.ph-card:hover { opacity: 1; border-color: var(--accent-2); }
.ph-emoji { font-size: 26px; opacity: 0.8; }
.ph-card h3 { font-size: 15.5px; font-weight: 700; color: var(--text-secondary); }
.ph-card p { font-size: 12.5px; color: var(--text-secondary); line-height: 1.5; opacity: 0.85; }

/* --- Navbar transparency at page top is now global (see the ===== NAVBAR
       ===== block); this body.home hook stays only for scene tweaks --- */
body.home .navbar:not(.scrolled):not(.top-hint) {
  box-shadow: none;
}

@media (max-width: 768px) {
  .scene::before,
  .scene::after { background-size: 100% 100%, cover; }
  .scene-cta .cta-wrap { padding-top: 90px; }
  .scene-interlude { height: 30vh; }
  /* mobile menu needs a solid navbar even at top — on every page */
  .navbar {
    background: var(--nav-bg);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border-bottom-color: var(--card-border);
  }
}

/* ===================================================================
   VISUAL PAGE EDITOR (admin only; injected by page-editor.js)
   =================================================================== */
body.ltm-ed-active { padding-top: 52px; }  /* clear the fixed toolbar */
/* Push the fixed navbar (and any fixed hero/scroll offsets) below the
   editor toolbar so the two bars don't overlap. */
body.ltm-ed-active .navbar { top: 52px; }
body.ltm-ed-active .scroll-top { bottom: 24px; }

.ltm-ed-toolbar {
  position: fixed; top: 0; left: 0; right: 0;
  height: 52px; z-index: 5000;
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 0 16px;
  background: var(--editor-bar-bg);
  color: var(--editor-bar-ink);
  box-shadow: 0 4px 18px rgba(0,0,0,0.35);
  font-size: 14px;
}
.ltm-ed-tb-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.ltm-ed-tb-right { display: flex; align-items: center; gap: 8px; }
.ltm-ed-badge { font-weight: 800; white-space: nowrap; }
.ltm-ed-page { font-size: 12px; opacity: 0.7; background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 20px; white-space: nowrap; }
.ltm-ed-status { font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ltm-ed-btn {
  border: none; border-radius: 8px; padding: 8px 14px;
  font-size: 13px; font-weight: 700; cursor: pointer;
  background: rgba(255,255,255,0.14); color: var(--text-on-accent); white-space: nowrap;
}
.ltm-ed-btn:hover { background: rgba(255,255,255,0.24); }
.ltm-ed-btn.primary { background: var(--editor-accent); }
.ltm-ed-btn.primary:hover { background: var(--editor-accent-hover); }
.ltm-ed-btn.ghost { background: transparent; border: 1px solid rgba(255,255,255,0.3); }
.ltm-ed-btn.full { width: 100%; margin-top: 6px; }

/* Editable targets */
.ltm-ed-target { outline: 1px dashed var(--editor-ring); outline-offset: 3px; cursor: pointer; transition: outline-color var(--dur-fast) var(--ease-standard); }
.ltm-ed-target:hover { outline: 2px dashed var(--editor-accent); }
.ltm-ed-selected { outline: 2px solid var(--editor-accent) !important; outline-offset: 3px; }

/* Style panel */
.ltm-ed-panel {
  position: fixed; top: 64px; right: 16px; z-index: 5001;
  width: 280px; max-height: calc(100vh - 84px); overflow-y: auto;
  background: var(--bg-secondary, #fff);
  color: var(--text-primary, #1f2a24);
  border: 1px solid var(--card-border, #e2e2e2);
  border-radius: 16px; padding: 16px;
  box-shadow: 0 20px 50px rgba(0,0,0,0.25);
}
.ltm-ed-panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.ltm-ed-x { background: none; border: none; font-size: 15px; cursor: pointer; color: var(--text-secondary); }
.ltm-ed-hint { font-size: 12px; color: var(--text-secondary); margin-bottom: 12px; line-height: 1.4; }
.ltm-ed-field { margin-bottom: 14px; }
.ltm-ed-field label { display: block; font-size: 12px; font-weight: 700; color: var(--text-secondary); margin-bottom: 6px; }
.ltm-ed-choices { display: flex; flex-wrap: wrap; gap: 6px; }
.ltm-ed-choice {
  border: 1px solid var(--card-border); background: var(--card-bg);
  color: var(--text-primary); border-radius: 8px;
  padding: 5px 10px; font-size: 12px; cursor: pointer;
}
.ltm-ed-choice.active { border-color: var(--accent-1); box-shadow: 0 0 0 2px var(--editor-ring-soft); }
.ltm-ed-choice.swatch { width: 28px; height: 28px; padding: 0; border-radius: 50%; }

/* Card style presets (applied to tagged cards) */
.cardstyle-flat { box-shadow: none !important; border: none !important; }
.cardstyle-outlined { box-shadow: none !important; border: 2px solid var(--accent-1) !important; }
.cardstyle-elevated { box-shadow: 0 18px 44px var(--shadow) !important; }

/* ===== ADMIN: APPEARANCE / BRAND THEME PICKER =====
   The swatch colours are set inline from JS (each theme's real accents), so
   they stay correct no matter which theme is currently active. */
.theme-help {
  font-size: 14px;
  color: var(--text-secondary);
  line-height: 1.6;
  margin-bottom: 18px;
  max-width: 640px;
}
.theme-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 14px;
  margin-bottom: 18px;
}
.theme-option {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 16px;
  border-radius: 14px;
  border: 2px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text-primary);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: var(--transition);
}
.theme-option:hover { border-color: var(--accent-1); transform: translateY(-2px); }
.theme-option.is-picked {
  border-color: var(--accent-1);
  box-shadow: 0 0 0 3px var(--shadow);
}
.theme-swatch {
  flex-shrink: 0;
  display: flex;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  overflow: hidden;
  border: 1px solid var(--card-border);
}
.theme-swatch-a, .theme-swatch-b { width: 50%; height: 100%; }
.theme-option-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.theme-option-text strong { font-size: 15px; font-weight: 700; }
.theme-option-text small { font-size: 12px; color: var(--text-secondary); line-height: 1.4; }
.theme-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.theme-actions .btn { padding: 10px 22px; font-size: 14px; }
.theme-actions .btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
.theme-current { font-size: 13px; font-weight: 600; color: var(--text-secondary); }

/* ===== HOMEPAGE CTA — per-user-state actions ===== */
.cta-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 6px; }

/* ============================================================
   SUBJECT-PAGE ITEM CARDS — the list under a subject's hero
   ============================================================
   Shared by BOTH subject pages, which are the same layout with a
   different leaf:
     • subject.html      (notes-render.js) → topic cards → the PDF viewer
     • exam-subject.html (exams-render.js) → exam cards  → the exam page
   Both set <body class="subject-page">, so everything here is scoped to
   that class. The section-card rules these sit under live further up
   (search ".subject-page .lib-sec-card").
   ------------------------------------------------------------ */

/* Hero typography & layout */
.subject-page .subject-hero h1 {
  text-transform: none;
  letter-spacing: -0.02em;
}
.subject-page .subject-hero-inner { padding: 0 8px; }
.subject-page .subject-hero-icon {
  display: flex;
  justify-content: center;
  width: 46px;
  height: 46px;
  margin: 0 auto 14px;
}
.subject-page .subject-hero-icon svg {
  width: 100%;
  height: 100%;
  display: block;
  stroke: url(#icon-gradient);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}
.subject-page .subject-topic-count { letter-spacing: 0.02em; }

/* Container — #topic-list on both subject pages, #examBody on exam.html */
.subject-page #topic-list,
.subject-page #examBody {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 0 16px 60px;
  max-width: 820px;
  margin: 0 auto;
}

/* Card shell — an <a> when the item is open, an <article> when it is locked */
.subject-page .lib-topic-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 16px;
  padding: 18px 22px 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  box-shadow: 0 2px 12px var(--shadow);
  transition: box-shadow var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-standard);
  position: relative;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}

/* Gradient accent bar on hover */
.subject-page .lib-topic-card::before {
  content: "";
  position: absolute;
  left: 0;
  top: 20%;
  bottom: 20%;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: linear-gradient(180deg, var(--accent-1), var(--accent-2));
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-standard);
}
.subject-page .lib-topic-card:hover::before { opacity: 1; }
.subject-page .lib-topic-card:hover {
  box-shadow: 0 12px 36px var(--shadow);
  transform: translateY(-2px);
}

/* Staggered entrance */
.subject-page .lib-topic-card:nth-child(1) { transition-delay: 0.06s; }
.subject-page .lib-topic-card:nth-child(2) { transition-delay: 0.12s; }
.subject-page .lib-topic-card:nth-child(3) { transition-delay: 0.18s; }
.subject-page .lib-topic-card:nth-child(4) { transition-delay: 0.24s; }
.subject-page .lib-topic-card:nth-child(5) { transition-delay: 0.30s; }

/* Row 1: number + title */
.subject-page .lib-topic-header {
  display: flex;
  align-items: baseline;
  gap: 14px;
}
.subject-page .lib-topic-num {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-1);
  background: rgba(99, 102, 241, 0.1);
  padding: 3px 10px;
  border-radius: 20px;
  font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
  flex-shrink: 0;
  line-height: 1.6;
}
.subject-page .lib-topic-title {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text-primary);
  margin: 0;
  letter-spacing: -0.01em;
  line-height: 1.35;
}

/* Row 2: description */
.subject-page .lib-topic-desc {
  font-size: 0.875rem;
  color: var(--text-secondary);
  margin: 0;
  line-height: 1.6;
  padding-left: 2px;
}

/* Row 3: action row */
.subject-page .lib-topic-actions {
  display: flex;
  align-items: center;
  padding-top: 6px;
  border-top: 1px solid var(--card-border);
  margin-top: 2px;
}

/* "View Notes" / "Start Test" — an indicator, not a standalone button
   (the whole card is the link) */
.subject-page .lib-btn-view {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--accent-1);
  background: none;
  border: none;
  padding: 0;
  cursor: inherit;
  font-family: inherit;
  letter-spacing: 0.01em;
  transition: gap var(--dur-fast) var(--ease-standard);
}
.subject-page .lib-topic-card:hover .lib-btn-view { gap: 10px; }
.subject-page .lib-btn-arrow {
  display: inline-block;
  transition: transform var(--dur-fast) var(--ease-standard);
}
.subject-page .lib-topic-card:hover .lib-btn-arrow { transform: translateX(3px); }

/* Locked / disabled state */
.subject-page .lib-btn-view--locked {
  color: var(--text-secondary);
  cursor: not-allowed;
  opacity: 0.6;
}
.subject-page .lib-icon-lock {
  width: 13px;
  height: 13px;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex-shrink: 0;
  vertical-align: middle;
}

/* Empty-state message */
.subject-page .lib-empty {
  color: var(--text-secondary);
  text-align: center;
  padding: 48px 0;
  font-size: 0.95rem;
}

/* Exam-only: the "25 questions · 30 min" line on an exam card, and the
   untimed/practice variant. Right-aligned in the action row, mirroring
   where a note card puts its "Updated ..." stamp. */
.subject-page .lib-exam-meta {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.78rem;
  color: var(--text-secondary);
  white-space: nowrap;
}
.subject-page .lib-exam-meta .lib-exam-timed {
  font-weight: 600;
  color: var(--accent-1);
}

@media (max-width: 600px) {
  .subject-page #topic-list,
  .subject-page #examBody { padding: 0 0 40px; }
  .subject-page .lib-topic-card {
    padding: 14px 16px 12px;
    border-radius: 12px;
  }
  .subject-page .lib-topic-header { gap: 10px; }
  .subject-page .lib-exam-meta { font-size: 0.72rem; }
}

/* ============================================================
   EXAM ENGINE — the attempt + results UI on exam.html
   ============================================================
   Rendered by exam-page.js inside #examBody (which .subject-page
   already sizes). Three screens share these pieces: the start card,
   the attempt (top bar + paper + question palette) and the results
   (stat tiles, split bar, review cards, leaderboard). Colours come
   only from existing tokens — no new palette here. */

.xm-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 16px;
  padding: 22px 24px;
  margin-bottom: 16px;
  box-shadow: 0 8px 24px var(--shadow);
}
.xm-h { font-size: 1.25rem; color: var(--text-primary); margin-bottom: 12px; }
.xm-h3 { font-size: 1.02rem; color: var(--text-primary); margin-bottom: 10px; }
.xm-dim { font-size: 0.8rem; color: var(--text-secondary); }
.xm-facts { list-style: none; padding: 0; margin: 0 0 14px; }
.xm-facts li {
  padding: 6px 0 6px 22px;
  position: relative;
  font-size: 0.92rem;
  color: var(--text-secondary);
}
.xm-facts li::before { content: '•'; position: absolute; left: 6px; color: var(--accent-1); }
.xm-standing { font-size: 0.9rem; color: var(--accent-1); font-weight: 600; margin-bottom: 12px; }

.xm-actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 14px; }
.xm-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 18px;
  border-radius: 10px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text-primary);
  font-size: 0.9rem; font-weight: 600; font-family: inherit;
  cursor: pointer; text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease-standard), transform var(--dur-fast) var(--ease-standard);
}
.xm-btn:hover { border-color: var(--accent-1); transform: translateY(-1px); }
.xm-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
.xm-btn-primary { background: var(--accent-1); border-color: var(--accent-1); color: var(--text-on-accent); }
.xm-btn-primary:hover { filter: brightness(1.06); }
/* "Correction?" in the results actions row. Brighter than a plain .xm-btn so
   it reads as an offer rather than a footnote, but tinted rather than solid —
   Retake is the primary action here and must stay the loudest thing in the
   row. Uses --accent-2 so it is visibly its own control beside Retake's
   --accent-1, and follows every brand theme (THEMES.md). */
.xm-btn-correction {
  background: color-mix(in srgb, var(--accent-2) 16%, transparent);
  border-color: var(--accent-2);
  color: var(--text-primary);   /* not --accent-2: it is a mid-tone in every
                                   theme and fails as text on a light tint */
  font-weight: 700;
}
.xm-btn-correction:hover {
  background: color-mix(in srgb, var(--accent-2) 26%, transparent);
  border-color: var(--accent-2);
}
.xm-btn-small { padding: 7px 12px; font-size: 0.8rem; }
/* Opt-in full-screen toggle in the exam topbar. exam-page.js only ever adds
   this on touch devices (mouse devices go full screen at Start instead), so
   it is sized for a fingertip outright — no media query needed, because it
   never renders on a desktop. */
.xm-fs-toggle {
  min-width: 44px; min-height: 44px;
  padding: 0; justify-content: center;
  font-size: 1.05rem; line-height: 1;
}

/* Attempt history rows (start screen) & leaderboard rows (results) */
.xm-hist-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 2px;
  border-bottom: 1px solid var(--card-border);
  font-size: 0.92rem; color: var(--text-primary);
}
.xm-hist-row:last-child { border-bottom: none; }
.xm-hist-row.you {
  background: var(--success-tint);
  border-radius: 10px;
  padding-left: 10px; padding-right: 10px;
}
.xm-best-tag {
  display: inline-block; margin-left: 8px; padding: 1px 8px;
  border-radius: 20px; font-size: 0.68rem; font-weight: 700;
  background: var(--success-tint); color: var(--success-ink);
}
.xm-lb-score {
  font-weight: 700; color: var(--text-primary); white-space: nowrap;
  text-align: right;
}
/* The figure the ranking actually sorts on — see ExamEngine._PCT_A. Quieter
   than the raw score, which is still what a student looks for first. */
.xm-lb-pct {
  display: block; font-size: 0.72rem; font-weight: 600;
  color: var(--text-secondary);
}

/* Leaderboard row identity: rank, avatar, then name over time taken. The rail
   is only 340px wide, so the name truncates rather than wrapping — which is
   why .xm-lb-id needs min-width: 0 (a flex item won't shrink below its content
   without it, and the ellipsis would never kick in). */
.xm-lb-who { display: flex; align-items: center; gap: 10px; min-width: 0; }
.xm-lb-rank {
  flex-shrink: 0; min-width: 22px;
  font-size: 0.82rem; font-weight: 700; color: var(--text-secondary);
}
.xm-lb-id { min-width: 0; }
.xm-lb-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The avatar the student picked, over their initial as the fallback. */
.xm-lb-avatar {
  position: relative; flex-shrink: 0;
  width: 32px; height: 32px;
  border-radius: 50%; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  color: var(--text-on-accent);
  font-size: 13px; font-weight: 700; line-height: 1;
}
.xm-lb-avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* ── Attempt: top bar ──
   A slim fixed strip across the whole screen (the takeover below has
   removed the navbar): title + progress pinned left, the clock dead
   centre, Submit in the top-right corner. The result screen reuses this
   strip (.xm-topbar-result) with an Exit exam button instead. */
.xm-topbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 40;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--card-bg);
  border-bottom: 1px solid var(--card-border);
  padding: 12px 24px;
  box-shadow: 0 4px 16px var(--shadow);
}
.xm-bar-title {
  max-width: 320px;
  font-size: 0.9rem; font-weight: 800; color: var(--text-primary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.xm-progress { font-size: 0.85rem; font-weight: 700; color: var(--text-secondary); white-space: nowrap; }
/* Save state (exam-page.js "answer durability"). Empty and invisible while
   every pick has reached the server — which is nearly always — so it costs no
   space until it has something to say. An unsaved pick is worth marks, so that
   state is loud and stays put rather than passing by as a toast. */
.xm-save-state { font-size: 0.78rem; font-weight: 700; white-space: nowrap; }
.xm-save-state:empty { display: none; }
.xm-save-state.saving { color: var(--text-secondary); }
.xm-save-state.unsaved {
  padding: 3px 10px; border-radius: 20px;
  background: var(--danger-tint); color: var(--danger-ink);
}
.xm-clock {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  font-size: 0.95rem; font-weight: 800; color: var(--accent-1);
  font-variant-numeric: tabular-nums;
}
.xm-clock.low { color: var(--danger-ink); }
.xm-btn-danger { border-color: var(--danger); color: var(--danger-ink); }
.xm-btn-danger:hover { background: var(--danger); border-color: var(--danger); color: #fff; }
/* "View results" wears a fixed indigo box so it stands apart from the
   theme accent and can't be missed, in every theme. Dark-mode variant
   below (kept off the token system on purpose — this accent is constant). */
.xm-btn-viewresults {
  border: 1.5px solid #6366f1;
  background: rgba(99, 102, 241, 0.10);
  color: #4338ca;
  font-weight: 800;
  box-shadow: 0 2px 12px rgba(99, 102, 241, 0.28);
}
.xm-btn-viewresults:hover { background: #6366f1; border-color: #6366f1; color: #fff; }
[data-mode="dark"] .xm-btn-viewresults {
  border-color: #818cf8;
  background: rgba(129, 140, 248, 0.16);
  color: #c7d2fe;
  box-shadow: 0 2px 12px rgba(129, 140, 248, 0.30);
}
[data-mode="dark"] .xm-btn-viewresults:hover { background: #6366f1; border-color: #6366f1; color: #fff; }
@media (max-width: 980px) {
  /* Mid widths: the absolutely-centred clock needs the left side kept
     short, so the title yields — the hero already named the test. */
  .xm-bar-title { display: none; }
}

/* ── Exam takeover & fullscreen ──
   From Start to Exit, exam-page.js keeps .exam-taking on <body> and the
   exam owns the window: navbar, hero, footer and scroll-top are gone,
   leaving the fixed topbar, the fixed question box and the navigator.
   On mouse devices Start also enters real browser fullscreen (.xm-fs on
   <html> from the fullscreenchange listener); leaving it mid-attempt prompts
   Resume/Exit rather than a toggle — a focus aid, not proctoring. Touch
   devices never enter fullscreen (see exam-page.js), so on a phone the
   takeover below is the whole effect — which is why every rule here works
   from .exam-taking alone and only adds html.xm-fs as an alternative. The
   takeover carries through to the result screen, left via Exit exam. */
:is(body.exam-taking, html.xm-fs body) .navbar,
:is(body.exam-taking, html.xm-fs body) .footer,
:is(body.exam-taking, html.xm-fs body) .scroll-top,
:is(body.exam-taking, html.xm-fs body) .subject-hero { display: none; }
/* Clearance under the fixed topbar. --xm-bar-h is the bar's MEASURED height,
   set on <body> by exam-page.js (see "topbar clearance") and kept current by a
   ResizeObserver — the bar wraps onto more rows with a long test title, a
   narrow screen, or the unsaved-answers pill, and a fixed padding then slid
   the navigator's heading underneath it. The fallback reproduces the old
   hardcoded 84px exactly, for the instant before the first measurement. */
:is(body.exam-taking, html.xm-fs body) .subject-page-wrap {
  padding: calc(var(--xm-bar-h, 56px) + 28px) 28px 28px;
}
/* #examBody is normally capped at a 820px reading column; the takeover needs
   the full width so the question box can fill from the left and the navigator
   can sit at the right edge. */
:is(body.exam-taking, html.xm-fs body) #examBody { max-width: none; padding: 0; }

@media (max-width: 640px) {
  /* No room for a centred clock next to the progress text — let it
     flow inline, and allow the strip two lines before content starts. */
  .xm-clock { position: static; transform: none; }
  /* Only the FALLBACK differs here: a phone wraps the bar onto more rows, so
     an unmeasured bar is assumed taller. --xm-bar-h wins whenever it is set. */
  :is(body.exam-taking, html.xm-fs body) .subject-page-wrap {
    padding-top: calc(var(--xm-bar-h, 90px) + 28px);
  }
}

/* ── Attempt: fixed question box + navigator ──
   The box on the left is sized to the viewport, never the question: a
   pinned header and footer with a scrolling middle (.xm-box-scroll), so a
   long question scrolls inside the box and the navigator on the right
   never moves. Paged is the only layout. */
.xm-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 24px; align-items: start;
}
.xm-paper {
  /* Clearance (measured bar + 28px) + 28px bottom pad + a little slack, so the
     box always ends on screen however many rows the topbar took. */
  height: calc(100vh - var(--xm-bar-h, 56px) - 68px);
  min-height: 340px;
  display: flex; flex-direction: column;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 16px;
  overflow: hidden;
}
.xm-box-head {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 13px 20px;
  border-bottom: 1px solid var(--card-border);
}
.xm-box-scroll { flex: 1 1 auto; overflow-y: auto; padding: 20px; }
.xm-box-foot {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 20px;
  border-top: 1px solid var(--card-border);
}
.xm-box-foot .xm-clear { margin-top: 0; }
.xm-box-nav { display: flex; gap: 8px; }
.xm-palette-wrap {
  position: sticky; top: calc(var(--xm-bar-h, 56px) + 28px);
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 14px;
  padding: 16px;
}
.xm-palette { display: grid; grid-template-columns: repeat(5, 1fr); gap: 7px; }
.xm-pal-btn {
  aspect-ratio: 1;
  border: 1px solid var(--card-border);
  border-radius: 9px;
  background: var(--bg-secondary);
  color: var(--text-secondary);
  font-size: 0.8rem; font-weight: 700; font-family: inherit;
  cursor: pointer;
}
.xm-pal-btn.answered { background: var(--success-tint); border-color: var(--success); color: var(--success-ink); }
.xm-pal-btn.seen { background: var(--danger-tint); border-color: var(--danger); color: var(--danger-ink); }
.xm-pal-btn.marked { box-shadow: inset 0 0 0 2px var(--gold-1); }
.xm-pal-btn.current { border-color: var(--accent-1); color: var(--accent-1); box-shadow: 0 0 0 2px var(--accent-1); }
.xm-legend { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
.xm-legend-item { display: inline-flex; align-items: center; gap: 5px; font-size: 0.72rem; color: var(--text-secondary); }
.xm-legend-dot { width: 10px; height: 10px; border-radius: 3px; display: inline-block; border: 1px solid var(--card-border); }
.xm-legend-dot.answered { background: var(--success-tint); border-color: var(--success); }
.xm-legend-dot.seen { background: var(--danger-tint); border-color: var(--danger); }
.xm-legend-dot.marked { box-shadow: inset 0 0 0 2px var(--gold-1); }
.xm-legend-dot.blank { background: var(--bg-secondary); }

/* ── Question card ── */
.xm-q { scroll-margin-top: 110px; }   /* clear of the fixed topbar, even two-line */
.xm-q-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.xm-q-no { font-size: 0.78rem; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-secondary); }
.xm-mark {
  border: none; background: none; cursor: pointer; font-family: inherit;
  font-size: 0.78rem; font-weight: 600; color: var(--text-secondary);
  padding: 4px 8px; border-radius: 8px;
}
.xm-mark:hover { background: var(--bg-secondary); }
.xm-mark.on { color: var(--gold-ink); background: rgba(230, 168, 23, 0.14); }
/* pre-line, because a question's line breaks carry meaning: a list of
   statements (i)…(iv), or a "match List I with List II" table flattened to one
   line per row (see app/question_format.py). The text is a text node, never
   markup, so the line break is the only structure it can have. Runs of spaces
   still collapse — the flattener uses an em space where a gap must survive. */
.xm-q-text { font-size: 1rem; color: var(--text-primary); line-height: 1.55; overflow-wrap: anywhere; white-space: pre-line; }
.xm-q-text .katex-display { overflow-x: auto; padding: 4px 0; }

.xm-opts { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.xm-opt {
  display: flex; align-items: flex-start; gap: 12px; text-align: left;
  width: 100%;
  padding: 11px 14px;
  border: 1.5px solid var(--card-border);
  border-radius: 12px;
  background: var(--bg-secondary);
  color: var(--text-primary);
  font-size: 0.94rem; font-family: inherit; line-height: 1.45;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-standard), background var(--dur-fast) var(--ease-standard);
}
button.xm-opt:hover { border-color: var(--accent-1); }
.xm-opt.selected {
  border-color: var(--accent-1);
  background: var(--card-bg);
  box-shadow: 0 0 0 1px var(--accent-1);
}
.xm-opt-key {
  flex: 0 0 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 7px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  font-size: 0.72rem; font-weight: 800; color: var(--text-secondary);
}
.xm-opt.selected .xm-opt-key { background: var(--accent-1); border-color: var(--accent-1); color: var(--text-on-accent); }
.xm-opt-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.xm-clear {
  margin-top: 10px; border: none; background: none; cursor: pointer;
  font-family: inherit; font-size: 0.78rem; font-weight: 600;
  color: var(--text-secondary); text-decoration: underline;
}
.xm-clear:hover { color: var(--danger-ink); }

/* ── Review states (results) ── */
.xm-opt.review { cursor: default; }
.xm-opt.right { border-color: var(--success); background: var(--success-tint); }
.xm-opt.right .xm-opt-key { background: var(--success); border-color: var(--success); color: #fff; }
.xm-opt.picked-wrong { border-color: var(--danger); background: var(--danger-tint); }
.xm-opt.picked-wrong .xm-opt-key { background: var(--danger); border-color: var(--danger); color: #fff; }
.xm-opt-tag { flex: 0 0 auto; font-size: 0.7rem; font-weight: 800; align-self: center; white-space: nowrap; }
.xm-opt-tag.ok { color: var(--success-ink); }
.xm-opt-tag.you { color: var(--text-secondary); }
.xm-verdict { font-size: 0.75rem; font-weight: 800; padding: 3px 10px; border-radius: 20px; }
.xm-verdict.ok { background: var(--success-tint); color: var(--success-ink); }
.xm-verdict.bad { background: var(--danger-tint); color: var(--danger-ink); }
.xm-verdict.none { background: var(--bg-secondary); color: var(--text-secondary); }
.xm-expl {
  margin-top: 14px;
  padding: 12px 14px;
  border-left: 3px solid var(--accent-1);
  border-radius: 0 10px 10px 0;
  background: var(--bg-secondary);
  font-size: 0.9rem; color: var(--text-primary); line-height: 1.55;
  overflow-wrap: anywhere; white-space: pre-line;
}
.xm-expl strong { display: block; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-secondary); margin-bottom: 4px; }
.xm-crowd { margin-top: 10px; }

/* ── Results: stat tiles + split bar ── */
.xm-expired {
  padding: 10px 14px; border-radius: 10px; margin-bottom: 14px;
  background: var(--danger-tint); border: 1px solid var(--danger-border);
  color: var(--danger-ink); font-size: 0.88rem; font-weight: 600;
}
/* A guest's result: not on any account until they sign in. */
.xm-guest-save {
  padding: 14px 16px; border-radius: 12px; margin-bottom: 14px;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-left: 3px solid var(--accent-1);
  color: var(--text-primary); font-size: 0.92rem; line-height: 1.5;
}
.xm-guest-save p { margin: 0; }
.xm-guest-save .xm-actions { margin-top: 10px; }
.xm-guest-save .xm-btn { text-decoration: none; }
.xm-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px; margin-bottom: 14px;
}
.xm-stat {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 14px;
  padding: 14px 16px;
}
.xm-stat-num { font-size: 1.45rem; font-weight: 800; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.xm-stat-lab { font-size: 0.74rem; color: var(--text-secondary); margin-top: 2px; }
.xm-stat-sub { font-size: 0.72rem; color: var(--accent-1); font-weight: 600; margin-top: 2px; }
.xm-split {
  display: flex; height: 10px; border-radius: 5px; overflow: hidden;
  border: 1px solid var(--card-border); background: var(--bg-secondary);
}
.xm-split-seg { display: block; height: 100%; }
.xm-split-seg.ok { background: var(--success); }
.xm-split-seg.bad { background: var(--danger); }
.xm-split-seg.none { background: var(--card-border); }
.xm-split-seg + .xm-split-seg { border-left: 2px solid var(--card-bg); }
.xm-split-lab { font-size: 0.8rem; color: var(--text-secondary); margin: 8px 0 4px; }
.xm-review-h { margin-top: 26px; }

/* Result screen: like the attempt, it fills the takeover width — the tiles
   and review spread across the space and the top-10 leaderboard pins to the
   right edge as a sticky rail (owner call, 2026-07-17). The side column is
   FIRST in the DOM (see exam-page.js) so the stacked mobile layout opens it
   under the actions row; the grid pins it right on wide screens. */
.xm-result-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 24px;
  align-items: start;
  margin-top: 10px;
}
.xm-result-side { grid-column: 2; grid-row: 1; position: sticky; top: 96px; }
/* Leaderboard switched off (config.LEADERBOARD_ENABLED): the rail is not
   rendered at all, so the review takes the whole width. */
.xm-result-cols.no-lb { grid-template-columns: minmax(0, 1fr); }
.xm-result-main { grid-column: 1; grid-row: 1; min-width: 0; }
.xm-result-main .xm-review-h { margin-top: 0; }
.xm-lb-card { margin: 0; }
.xm-lb-toggle { display: none; }   /* the rail is always visible on desktop */
@media (max-width: 960px) {
  .xm-result-cols { display: block; }
  .xm-result-side { position: static; display: none; margin-bottom: 18px; }
  .xm-result-cols.lb-open .xm-result-side { display: block; }
  .xm-lb-toggle { display: inline-flex; }
  .xm-result-main .xm-review-h { margin-top: 26px; }
}

@media (max-width: 820px) {
  .xm-grid { grid-template-columns: 1fr; }
  .xm-palette-wrap { position: static; order: -1; }
  .xm-palette { grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); }
  /* On narrow/touch screens a fixed box with a nested scroll fights the
     page scroll — let the box grow and the page scroll instead. */
  .xm-paper { height: auto; min-height: 0; }
  .xm-box-scroll { overflow-y: visible; }
}

/* ── Confirm dialog (submit / leave / exit) ──
   A plain scrim, no backdrop-filter: the hero blobs are hidden under the
   takeover, and full-viewport blur stalls weak GPUs (see CLAUDE.md). */
.xm-modal-overlay {
  position: fixed; inset: 0; z-index: 3000;
  background: var(--scrim);
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.xm-modal {
  width: 100%; max-width: 400px;
  background: var(--bg-secondary);
  border: 1px solid var(--card-border);
  border-radius: 18px;
  padding: 26px;
  box-shadow: 0 24px 60px var(--shadow);
}
.xm-modal h3 { font-size: 1.15rem; font-weight: 800; margin-bottom: 8px; color: var(--text-primary); }
.xm-modal p { font-size: 0.92rem; color: var(--text-secondary); line-height: 1.5; margin-bottom: 20px; }
.xm-modal-actions { display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap; }


/* ============================================================
   CORRECTIONS — "Suggest a correction"
   ============================================================
   One feature across three surfaces, kept in one block so it stays
   one visual family:
     1. the popup itself (global — the note viewer and the exam page
        both open it from script.js),
     2. the per-question link on an exam review card (.xm-*),
     3. the admin dashboard's Corrections card (.page-admin).
   Tokens only — no colour is hardcoded, so it follows every brand
   theme and both modes (see THEMES.md).
   ============================================================ */

/* 1. The popup. Plain scrim, no backdrop-filter — same reason the exam
      confirm dialog above has none (CLAUDE.md). */
.corr-overlay {
  position: fixed; inset: 0; z-index: 3200;
  background: var(--scrim);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  overflow-y: auto;
}
.corr-modal {
  position: relative;
  width: 100%; max-width: 520px;
  background: var(--bg-secondary);
  border: 1px solid var(--card-border);
  border-radius: 18px;
  padding: 26px;
  box-shadow: 0 24px 60px var(--shadow);
  margin: auto;
}
.corr-close {
  position: absolute; top: 12px; right: 12px;
  width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid var(--card-border); background: transparent;
  color: var(--text-secondary); font-size: 15px; cursor: pointer; line-height: 1;
}
.corr-close:hover { color: var(--text-primary); border-color: var(--accent-1); }
.corr-title {
  font-size: 1.18rem; font-weight: 800; color: var(--text-primary);
  margin: 0 34px 6px 0;
}
.corr-lede { font-size: 0.9rem; color: var(--text-secondary); line-height: 1.5; margin-bottom: 14px; }
/* What the correction will be attached to. Shown, never typed — the
   student should not have to explain where they are. */
.corr-target {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  /* A neutral shade, not a token: this must read as "inset panel" against
     --bg-secondary in BOTH modes, and every surface token is near-white in
     light mode. A grey with no hue stays brand-agnostic. */
  background: rgba(127, 127, 127, 0.09);
  border: 1px solid var(--card-border);
  border-radius: 10px; padding: 10px 12px; margin-bottom: 16px;
  font-size: 0.85rem; color: var(--text-secondary);
}
.corr-crumb { word-break: break-word; }
.corr-crumb.strong { color: var(--text-primary); font-weight: 700; }
.corr-sep { color: var(--text-secondary); opacity: 0.6; }
.corr-detail {
  flex-basis: 100%; font-size: 0.8rem; font-weight: 600;
  color: var(--accent-1);
}
.corr-label { display: block; font-size: 0.82rem; font-weight: 700; color: var(--text-secondary); margin-bottom: 6px; }
.corr-text {
  width: 100%; padding: 11px 13px; border-radius: 10px;
  border: 1px solid var(--card-border);
  background: var(--card-bg); color: var(--text-primary);
  font-family: inherit; font-size: 0.95rem; line-height: 1.5; resize: vertical;
}
.corr-text:focus { outline: 2px solid var(--accent-1); outline-offset: 1px; border-color: transparent; }
.corr-foot { display: flex; justify-content: flex-end; margin-top: 5px; }
.corr-count { font-size: 0.75rem; color: var(--text-secondary); }
.corr-note { font-size: 0.92rem; color: var(--text-secondary); line-height: 1.55; margin-bottom: 18px; }
.corr-actions { display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap; margin-top: 14px; }
.corr-btn {
  min-height: 44px; padding: 10px 18px; border-radius: 10px;
  font-family: inherit; font-size: 0.9rem; font-weight: 700;
  cursor: pointer; border: 1px solid transparent;
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none;
}
.corr-btn.primary {
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  background-origin: border-box;   /* see THEMES.md — stops the border rim */
  color: #fff;
}
.corr-btn.primary:disabled { opacity: 0.6; cursor: default; }
.corr-btn.secondary {
  background: transparent; border-color: var(--card-border); color: var(--text-primary);
}
.corr-btn.secondary:hover { border-color: var(--accent-1); }
.corr-error { font-size: 0.85rem; color: var(--danger); margin-top: 10px; text-align: right; }

/* 2. The optional "which question?" picker (exam corrections only). */
.corr-select {
  width: 100%; min-height: 44px; padding: 10px 12px; border-radius: 10px;
  border: 1px solid var(--card-border);
  background: var(--card-bg); color: var(--text-primary);
  font-family: inherit; font-size: 0.9rem; margin-bottom: 14px;
}
.corr-select:focus { outline: 2px solid var(--accent-1); outline-offset: 1px; }

/* 3. The unresolved badge on the navbar avatar (owner only), and the
      matching count on the dropdown's Corrections link. */
.dropdown-pill {
  display: inline-block; min-width: 18px; padding: 0 6px; margin-left: 4px;
  border-radius: 20px; background: var(--danger); color: #fff;
  font-size: 11px; font-weight: 800; line-height: 18px; text-align: center;
}
.avatar-badge {
  position: absolute; top: -7px; right: -9px;   /* clear of the initial/photo */
  min-width: 19px; height: 19px; padding: 0 5px;
  border-radius: 20px; background: var(--danger); color: #fff;
  font-size: 11px; font-weight: 800; line-height: 19px; text-align: center;
  box-shadow: 0 0 0 2px var(--bg-secondary);
}

/* 4. "Corrections you've sent" on the student's own dashboard. */
.page-dashboard .dash-corr-help {
  font-size: 13px; color: var(--text-secondary); line-height: 1.55; margin: 4px 0 14px;
}
.page-dashboard .dash-corr-list { display: flex; flex-direction: column; gap: 10px; }
.page-dashboard .dash-corr-row {
  border: 1px solid var(--card-border); border-radius: 10px; padding: 12px 14px;
}
.page-dashboard .dash-corr-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
}
.page-dashboard .dash-corr-where {
  font-size: 14px; font-weight: 700; color: var(--text-primary); text-decoration: none;
}
a.dash-corr-where:hover { color: var(--accent-1); text-decoration: underline; }
.page-dashboard .dash-corr-tag {
  font-size: 11.5px; font-weight: 800; padding: 3px 10px; border-radius: 20px;
  white-space: nowrap;
}
.page-dashboard .dash-corr-tag.open {
  background: rgba(127, 127, 127, 0.16); color: var(--text-secondary);
}
.page-dashboard .dash-corr-tag.done { background: var(--success); color: #fff; }
.page-dashboard .dash-corr-detail {
  font-size: 12px; font-weight: 600; color: var(--accent-1); margin-top: 3px;
}
.page-dashboard .dash-corr-msg {
  font-size: 13.5px; color: var(--text-secondary); line-height: 1.55;
  margin: 8px 0 0; white-space: pre-wrap; word-break: break-word;
}
/* The answer, under what they wrote and clearly not part of it. */
.page-dashboard .dash-corr-reply {
  margin-top: 10px; padding: 10px 12px; border-radius: 9px;
  border-left: 3px solid var(--success);
  background: rgba(127, 127, 127, 0.09);
}
.page-dashboard .dash-corr-reply-label {
  display: block; font-size: 11.5px; font-weight: 800; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--success);
}
.page-dashboard .dash-corr-reply-msg {
  font-size: 13.5px; color: var(--text-primary); line-height: 1.55;
  margin: 5px 0 0; white-space: pre-wrap; word-break: break-word;
}
.page-dashboard .dash-corr-when {
  display: block; font-size: 11.5px; color: var(--text-secondary);
  opacity: 0.8; margin-top: 8px;
}

/* 5. The admin dashboard's Corrections card. */
.page-admin .corr-help {
  font-size: 13.5px; color: var(--text-secondary); line-height: 1.55;
  margin-bottom: 16px;
}
.page-admin .corr-switch {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding-bottom: 12px; border-bottom: 1px solid var(--card-border);
}
.page-admin .corr-side {
  min-height: 44px; padding: 9px 16px; border-radius: 10px;
  border: 1px solid var(--card-border); background: transparent;
  color: var(--text-secondary); font-family: inherit; font-size: 14px;
  font-weight: 700; cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px;
}
.page-admin .corr-side:hover { border-color: var(--accent-1); color: var(--text-primary); }
.page-admin .corr-side.active {
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  background-origin: border-box;   /* see THEMES.md */
  color: #fff; border-color: transparent;
}
.page-admin .corr-pill {
  min-width: 20px; padding: 1px 7px; border-radius: 20px;
  background: var(--danger); color: #fff; font-size: 11.5px; font-weight: 800;
}
.page-admin .corr-side.active .corr-pill { background: rgba(0, 0, 0, 0.28); }
.page-admin #corrStatus {
  min-height: 44px; padding: 9px 12px; border-radius: 10px;
  border: 1px solid var(--card-border); background: var(--card-bg);
  color: var(--text-primary); font-family: inherit; font-size: 14px;
}
.page-admin .corr-list { display: flex; flex-direction: column; gap: 12px; margin-top: 16px; }
.page-admin .corr-empty { font-size: 14px; color: var(--text-secondary); padding: 24px 0; text-align: center; }
.page-admin .corr-item {
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-left: 4px solid var(--accent-1);
  border-radius: 12px; padding: 14px 16px;
}
.page-admin .corr-item.resolved { border-left-color: var(--success); opacity: 0.72; }
.page-admin .corr-item-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
}
.page-admin .corr-where {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px;
  font-size: 13px; color: var(--text-secondary);
}
.page-admin .corr-where .corr-crumb.strong { font-size: 14.5px; color: var(--text-primary); font-weight: 700; }
.page-admin .corr-open {
  font-size: 13px; font-weight: 700; color: var(--accent-1);
  text-decoration: none; white-space: nowrap;
  min-height: 44px; display: inline-flex; align-items: center;
}
.page-admin .corr-open:hover { text-decoration: underline; }
.page-admin .corr-pin {
  font-size: 12.5px; font-weight: 600; color: var(--accent-1); margin-top: 2px;
  /* Two lines, then ellipsis. The question is quoted whole (a slice would cut
     inside a $…$ and break the typesetting), so the clamp is visual only. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.page-admin .corr-qtext { font-weight: 400; color: var(--text-secondary); }
/* KaTeX inside that clamp must not push the line box taller than the text. */
.page-admin .corr-qtext .katex { font-size: 1em; }
.page-admin .corr-msg {
  font-size: 14px; color: var(--text-primary); line-height: 1.55;
  background: rgba(127, 127, 127, 0.09); border-radius: 9px;   /* see .corr-target */
  padding: 10px 12px; margin: 10px 0 0;
  white-space: pre-wrap; word-break: break-word;
}
.page-admin .corr-item-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-top: 12px;
}
.page-admin .corr-who {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--text-secondary);
}
.page-admin .corr-who .avatar { width: 26px; height: 26px; font-size: 12px; }
.page-admin .corr-who-name { font-weight: 700; color: var(--text-primary); }
.page-admin .corr-resolved-at { font-size: 12px; color: var(--success); margin-top: 8px; font-weight: 600; }
/* Folded away until "Reply & resolve" is clicked — see admin.html. */
.page-admin .corr-reply-form { margin-top: 12px; }
.page-admin .corr-reply-box {
  width: 100%; min-height: 76px; padding: 10px 12px; border-radius: 10px;
  border: 1px solid var(--card-border); background: var(--card-bg);
  color: var(--text-primary); font-family: inherit; font-size: 14px;
  line-height: 1.5; resize: vertical;
}
.page-admin .corr-reply-box:focus {
  outline: none; border-color: var(--accent-1);
}
.page-admin .corr-reply-actions {
  display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px;
}
.page-admin .corr-btn.ghost {
  background: none; border: 1px solid var(--card-border); color: var(--text-secondary);
}
/* What the student is reading right now, kept on the resolved row. */
.page-admin .corr-reply-sent {
  margin-top: 10px; padding: 10px 12px; border-radius: 9px;
  border-left: 3px solid var(--success); background: rgba(127, 127, 127, 0.09);
}
.page-admin .corr-reply-sent-label {
  display: block; font-size: 11.5px; font-weight: 800; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--success);
}
.page-admin .corr-reply-sent-msg {
  font-size: 13.5px; color: var(--text-primary); line-height: 1.55;
  margin: 5px 0 0; white-space: pre-wrap; word-break: break-word;
}


/* ============================================================
   PAGE-SCOPED CSS — moved verbatim from per-page <style> blocks.
   Each section applies only under its body class; same class names
   on different pages are DIFFERENT widgets that drifted apart while
   the CSS lived in the HTML (e.g. .toggle-password exists three
   times with three bodies). Unifying them is a design decision, not
   cleanup — do it deliberately, page by page, with screenshots.
   ============================================================ */
@keyframes spin {
  to { transform: rotate(360deg); }
}   /* used by page-register */

/* ===== PAGE: 404.html (scoped .page-404; moved verbatim from its <style> block) ===== */
.page-404 .nf-wrap {
    min-height: calc(100vh - 200px);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 140px 24px 80px;
    text-align: center;
  }
.page-404 .nf-inner { max-width: 460px; }
.page-404 .nf-code {
    font-size: 96px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.04em;
    background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    margin-bottom: 12px;
  }
.page-404 .nf-inner h1 {
    font-size: 26px;
    color: var(--text-primary);
    margin-bottom: 10px;
    letter-spacing: -0.01em;
  }
.page-404 .nf-inner p {
    color: var(--text-secondary);
    font-size: 15px;
    line-height: 1.7;
    margin-bottom: 28px;
  }
.page-404 .nf-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.page-404 .nf-btn {
    display: inline-block;
    padding: 12px 24px;
    border-radius: 12px;
    font-size: 14.5px;
    font-weight: 600;
    text-decoration: none;
    background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
    color: #fff;
    transition: var(--transition);
  }
.page-404 .nf-btn:hover { transform: translateY(-2px); box-shadow: 0 10px 26px var(--shadow); }
.page-404 .nf-btn.secondary {
    background: var(--card-bg);
    color: var(--text-primary);
    border: 1px solid var(--card-border);
  }

/* ===== PAGE: admin.html (scoped .page-admin; moved verbatim from its <style> block) ===== */
.page-admin .admin-container {
    max-width: 1200px;
    /* Top margin clears the fixed navbar */
    margin: 120px auto 40px;
    padding: 0 20px;
    display: flex;
    flex-direction: column;
    gap: 24px;
}
.page-admin .admin-card {
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: 20px;
    padding: 24px;
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    box-shadow: 0 12px 32px var(--shadow);
}
.page-admin .admin-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 24px;
}
.page-admin .admin-header h1 {
    font-size: 24px;
    color: var(--text-primary);
}
.page-admin /* Stats */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
}
.page-admin .stat-tile {
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: 14px;
    padding: 18px 20px;
}
.page-admin .stat-tile .stat-value { font-size: 30px; font-weight: 800; color: var(--accent-1); }
.page-admin .stat-tile .stat-label { font-size: 13px; color: var(--text-secondary); margin-top: 4px; }
.page-admin .charts-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 24px;
}
.page-admin .chart-card h3, .page-admin .list-card h3 { font-size: 16px; color: var(--text-primary); margin-bottom: 14px; }
.page-admin .bar-chart {
    display: flex;
    align-items: flex-end;
    gap: 3px;
    height: 120px;
}
.page-admin .bar-chart .bar {
    flex: 1;
    min-width: 4px;
    background: var(--accent-1);
    border-radius: 3px 3px 0 0;
    opacity: 0.85;
}
.page-admin .bar-chart .bar:hover { opacity: 1; }
.page-admin .chart-empty { color: var(--text-secondary); font-size: 14px; padding: 32px 0; text-align: center; width: 100%; }
.page-admin .simple-list { list-style: none; padding: 0; }
.page-admin .simple-list li {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 9px 2px;
    border-bottom: 1px solid var(--card-border);
    font-size: 14px;
}
.page-admin .simple-list li:last-child { border-bottom: none; }
.page-admin .muted { color: var(--text-secondary); font-size: 12.5px; white-space: nowrap; }
.page-admin /* Users table */
.controls {
    display: flex;
    gap: 16px;
    margin-bottom: 24px;
}
.page-admin .controls input, .page-admin .controls select {
    padding: 10px 16px;
    border-radius: 8px;
    border: 1px solid var(--card-border);
    background: var(--bg-secondary);
    color: var(--text-primary);
    font-size: 14px;
}
.page-admin .controls input {
    flex: 1;
}
.page-admin table {
    width: 100%;
    border-collapse: collapse;
}
.page-admin th, .page-admin td {
    padding: 16px;
    text-align: left;
    border-bottom: 1px solid var(--card-border);
    font-size: 14px;
}
.page-admin th {
    font-weight: 600;
    color: var(--text-secondary);
    background: var(--bg-secondary);
}
.page-admin tr:hover {
    background: rgba(0,0,0,0.02);
}
.page-admin .avatar-cell {
    display: flex;
    align-items: center;
    gap: 12px;
}
.page-admin .avatar {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--accent-1);
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
    font-size: 14px;
    object-fit: cover;
}
.page-admin .role-badge {
    padding: 4px 10px;
    border-radius: 20px;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
}
.page-admin .role-admin {
    background: rgba(255, 193, 7, 0.2);
    color: #b38600;
}
.page-admin .role-user {
    background: rgba(79, 156, 91, 0.14);
    color: var(--accent-1);
}
/* The one account with is_root set. ADMIN is still set only with
   `python -m app.make_admin`, so this badge and role-admin are labels; the
   Editor badge beside them is the one role this page can actually write. */
.page-admin .role-root {
    background: rgba(108, 92, 231, 0.18);
    color: #5b4bc4;
}
.page-admin .role-editor {
    background: rgba(23, 162, 184, 0.18);
    color: #0f6674;
}
.page-admin .pagination {
    display: flex;
    justify-content: center;
    gap: 8px;
    margin-top: 24px;
}
.page-admin .pagination button {
    padding: 8px 16px;
    border-radius: 6px;
    border: 1px solid var(--card-border);
    background: var(--bg-secondary);
    color: var(--text-primary);
    cursor: pointer;
}
.page-admin .pagination button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}
.page-admin .message {
    padding: 12px;
    border-radius: 8px;
    margin-bottom: 16px;
    display: none;
}
.page-admin .message.success {
    background: rgba(40, 167, 69, 0.1);
    color: #28a745;
    border: 1px solid rgba(40, 167, 69, 0.2);
}
.page-admin .message.error {
    background: rgba(220, 53, 69, 0.1);
    color: #dc3545;
    border: 1px solid rgba(220, 53, 69, 0.2);
}

/* ----- Making someone an Editor, and the logbook -----
   Tokens only (THEMES.md): the two badges above predate the token set and
   keep their literal colours, but nothing new here does. */
.page-admin .role-btn {
    padding: 6px 12px;
    border-radius: 8px;
    border: 1px solid var(--card-border);
    background: var(--bg-secondary);
    color: var(--text-primary);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.page-admin .role-btn:hover { border-color: var(--accent-1); }
.page-admin .role-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.page-admin .role-btn.primary {
    background: var(--accent-solid);
    border-color: var(--accent-solid-border);
    color: var(--text-on-accent);
}

/* A plain dark scrim, not a blur: a full-viewport backdrop-filter is banned
   site-wide (CLAUDE.md) because it is recomputed every frame. */
.page-admin .role-modal-back {
    position: fixed;
    inset: 0;
    background: var(--scrim-strong);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    z-index: 3000;
}
.page-admin .role-modal-back[hidden] { display: none; }
.page-admin .role-modal {
    width: min(440px, 100%);
    max-height: 90vh;
    overflow-y: auto;
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: 16px;
    padding: 22px;
    box-shadow: 0 18px 48px var(--shadow);
}
.page-admin .role-modal h3 { margin: 0 0 10px; color: var(--text-primary); }
.page-admin .role-modal label {
    display: block;
    margin: 14px 0 6px;
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
}
.page-admin .role-modal input {
    width: 100%;
    padding: 10px 12px;
    border-radius: 8px;
    border: 1px solid var(--card-border);
    background: var(--input-bg);
    color: var(--text-primary);
    font-size: 15px;
}
.page-admin .role-modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 20px;
}
.page-admin .role-modal .message { display: block; margin: 14px 0 0; }

.page-admin .log-controls {
    display: flex;
    gap: 18px;
    flex-wrap: wrap;
    margin: 12px 0 14px;
    font-size: 14px;
    color: var(--text-secondary);
}
.page-admin .log-controls label {
    display: flex;
    align-items: center;
    gap: 7px;
    cursor: pointer;
}
.page-admin .log-list { display: flex; flex-direction: column; gap: 8px; }
.page-admin .log-row {
    border: 1px solid var(--card-border);
    border-left: 3px solid var(--card-border);
    border-radius: 10px;
    background: var(--bg-secondary);
    padding: 10px 14px;
}
/* An edit that threw away most of a note. Marked, not hidden — it is still a
   legitimate change, it is just the one worth reading. */
.page-admin .log-row.flagged { border-left-color: var(--accent-1); }
.page-admin .log-what { color: var(--text-primary); font-size: 14.5px; }
.page-admin .log-who {
    color: var(--text-secondary);
    font-size: 13px;
    margin-top: 3px;
}

@media (hover: none) and (pointer: coarse) {
    .page-admin .role-btn { min-height: 44px; }
    .page-admin .log-controls label { min-height: 44px; }
}

/* ===== PAGE: content-manager.html (scoped .page-cm; moved verbatim from its <style> block) ===== */
.page-cm .cm-container {
    max-width: 1200px;
    /* Top margin clears the fixed navbar */
    margin: 120px auto 40px;
    padding: 0 20px;
}
.page-cm .cm-card {
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: 20px;
    padding: 24px;
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    box-shadow: 0 12px 32px var(--shadow);
}
.page-cm .cm-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    flex-wrap: wrap;
    gap: 12px;
}
.page-cm .cm-header h1 { font-size: 24px; color: var(--text-primary); }
.page-cm .cm-tabs { display: flex; gap: 8px; margin-bottom: 24px; border-bottom: 1px solid var(--card-border); }
.page-cm .cm-tab {
    padding: 10px 18px;
    border: none;
    background: none;
    font-size: 15px;
    font-weight: 600;
    color: var(--text-secondary);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}
.page-cm .cm-tab.active { color: var(--accent-1); border-bottom-color: var(--accent-1); }
.page-cm .cm-tab:disabled { opacity: 0.45; cursor: not-allowed; }
.page-cm .cm-toolbar { display: flex; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; align-items: center; }
.page-cm .cm-toolbar select {
    padding: 10px 14px;
    border-radius: 8px;
    border: 1px solid var(--card-border);
    background: var(--bg-secondary);
    color: var(--text-primary);
    font-size: 14px;
    min-width: 220px;
}
.page-cm .cm-btn {
    padding: 9px 16px;
    border-radius: 8px;
    border: none;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    background: var(--accent-1);
    /* Not #fff: azure and indigo put DARK ink on an accent fill in dark mode,
       so the literal made every primary admin button unreadable there. */
    color: var(--text-on-accent);
}
.page-cm .cm-btn.secondary { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--card-border); }
.page-cm .cm-btn.danger { background: #dc3545; }
.page-cm .cm-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.page-cm table { width: 100%; border-collapse: collapse; }
.page-cm th, .page-cm td {
    padding: 12px 14px;
    text-align: left;
    border-bottom: 1px solid var(--card-border);
    font-size: 14px;
    vertical-align: middle;
}
.page-cm th { font-weight: 600; color: var(--text-secondary); background: var(--bg-secondary); }
.page-cm td .cm-btn { padding: 6px 10px; font-size: 12px; margin-right: 6px; }
.page-cm td select { padding: 6px 8px; border-radius: 6px; border: 1px solid var(--card-border); background: var(--bg-secondary); color: var(--text-primary); font-size: 13px; }
.page-cm .cm-badge {
    display: inline-block;
    padding: 3px 10px;
    border-radius: 20px;
    font-size: 11.5px;
    font-weight: 600;
    text-transform: uppercase;
}
.page-cm .cm-badge.available { background: rgba(76, 175, 80, 0.15); color: #388e3c; }
.page-cm .cm-badge.coming-soon { background: rgba(255, 152, 0, 0.15); color: #b37400; }
.page-cm .cm-badge.scheduled { background: rgba(76, 175, 80, 0.15); color: #388e3c; }
.page-cm .cm-badge.completed { background: rgba(120, 120, 120, 0.15); color: var(--text-secondary); }
.page-cm .cm-badge.cancelled { background: rgba(220, 53, 69, 0.15); color: #c62828; }
.page-cm .cm-badge.draft { background: rgba(120, 120, 120, 0.15); color: var(--text-secondary); }
/* Derived live state of a class row (Classes tab) — not the same axis as
   the status badges above, which is why "live" gets the loud colour. */
.page-cm .cm-badge.live { background: rgba(220, 53, 69, 0.9); color: #fff; }
.page-cm .cm-badge.upcoming { background: rgba(33, 150, 243, 0.15); color: #1565c0; }
.page-cm .cm-badge.ended { background: rgba(120, 120, 120, 0.15); color: var(--text-secondary); }
.page-cm .cm-feat { cursor: pointer; font-size: 18px; background: none; border: none; }

/* ----- the two ticks (Checked column) -----
   One button per person: the editor presses theirs on the Editing Desk, the
   owner presses his in the content manager. Off is a plain outline, on is the
   semantic green — the same --success the rest of the site uses for "this is
   settled", not the brand accent, because it is a state and not an action.
   Ink on the green fill is spelled out rather than inherited: this fill is the
   same in both themes, so it takes the same ink in both (THEMES.md). */
.page-cm .cm-tick {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 5px 10px;
    border-radius: 7px;
    border: 1px solid var(--card-border);
    background: var(--bg-secondary);
    color: var(--text-secondary);
    font-size: 12px; font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
}
.page-cm .cm-tick:hover { border-color: var(--success); color: var(--text-primary); }
.page-cm .cm-tick.on {
    background: var(--success);
    border-color: var(--success);
    color: #062f22;
}
.page-cm .cm-tick:disabled { opacity: 0.6; cursor: wait; }

/* The other person's mark, under the button. Read-only wherever it appears:
   the editor cannot set the owner's and the owner does not set the editor's. */
.page-cm .cm-tick-note {
    display: block;
    margin-top: 5px;
    font-size: 11.5px;
    font-weight: 600;
    color: var(--success-ink);
}
.page-cm .cm-tick-note.waiting { color: var(--text-secondary); font-weight: 500; }

/* "3 waiting for your tick", on the toolbar of the Notes and Exams tables. */
.page-cm .cm-waiting {
    display: inline-flex; align-items: center;
    padding: 6px 12px;
    border-radius: 50px;
    border: 1px solid var(--success);
    background: var(--success-tint);
    color: var(--success-ink);
    font-size: 12.5px; font-weight: 600;
}
/* `display: inline-flex` above beats the UA's `[hidden] { display: none }`,
   so without this the chip reads "0 waiting" forever (CLAUDE.md). */
.page-cm .cm-waiting[hidden] { display: none; }
.page-cm .cm-empty { text-align: center; padding: 32px; color: var(--text-secondary); }
.page-cm .cm-msg { padding: 12px; border-radius: 8px; margin-bottom: 16px; display: none; font-size: 14px; }
.page-cm .cm-msg.success { background: rgba(40, 167, 69, 0.1); color: #28a745; border: 1px solid rgba(40, 167, 69, 0.2); }
.page-cm .cm-msg.error { background: rgba(220, 53, 69, 0.1); color: #dc3545; border: 1px solid rgba(220, 53, 69, 0.2); }
/* Work in progress, not an outcome — used while a LaTeX compile is running,
   which is the one admin action that takes long enough to need saying. */
.page-cm .cm-msg.info { background: rgba(33, 150, 243, 0.1); color: #1976d2; border: 1px solid rgba(33, 150, 243, 0.22); }
.page-cm /* Modal */
/* Plain darker scrim, no backdrop-filter: a full-viewport blur is banned
   (CLAUDE.md) — it recomputes every frame and made anything behind it an
   unreadable smear. The modal carries its own messages (.cm-modal-msg). */
.cm-overlay {
    position: fixed; inset: 0; z-index: 3000;
    background: rgba(8, 16, 11, 0.72);
    display: flex; align-items: center; justify-content: center;
    padding: 24px;
}
/* Validation errors raised from inside a modal render here, above the
   actions row — never on the page banner hidden behind the scrim. */
.page-cm .cm-modal-msg { margin: 16px 0 0; }

/* ----- Two-column modal (the Schedule/Edit Class form) -----
   A class carries far more fields than a note or a subject, and stacked in
   one column the Save button ended up a long scroll below the icon pickers.
   Splitting into "the session" and "the class page" keeps the whole form —
   and the actions row — on screen. The actions row and the message slot stay
   direct children of .cm-modal, so both still span the full width below. */
.page-cm .cm-modal.cols { max-width: 940px; }
.page-cm .cm-modal-cols {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 28px;
    align-items: start;
}
.page-cm .cm-col-head {
    font-size: 12px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--accent-1);
    padding-bottom: 8px;
    margin-bottom: 4px;
    border-bottom: 1px solid var(--card-border);
}
/* ----- Collapsed icon palettes (the class modal) -----
   The trigger shows the current pick; the grid unfolds under it. */
.page-cm .cm-icon-drop { position: relative; margin-top: 2px; }
.page-cm .cm-icon-drop-btn {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 9px 12px;
    border: 1px solid var(--card-border);
    border-radius: 10px;
    background: var(--card-bg);
    color: var(--text-primary);
    font-size: 14px;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: border-color var(--dur-fast) var(--ease-standard);
}
.page-cm .cm-icon-drop-btn:hover { border-color: var(--accent-1); }
.page-cm .cm-icon-drop-btn[aria-expanded="true"] { border-color: var(--accent-1); }
.page-cm .cm-icon-drop-preview { display: flex; gap: 4px; flex-shrink: 0; }
.page-cm .cm-icon-drop-chip { display: flex; }
.page-cm .cm-icon-drop-chip svg { width: 20px; height: 20px; stroke: var(--accent-1); }
.page-cm .cm-icon-drop-text {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-secondary);
}
.page-cm .cm-icon-drop-caret { flex-shrink: 0; color: var(--text-secondary); }
.page-cm .cm-icon-drop-panel {
    margin-top: 8px;
    max-height: 168px;
    overflow-y: auto;
    padding: 8px;
    border: 1px solid var(--card-border);
    border-radius: 10px;
    background: var(--bg-primary);
}
/* .cm-icon-grid sets display:grid, and an author rule beats the UA's
   [hidden] { display: none } — without this the panel never closes. */
.page-cm .cm-icon-drop-panel[hidden] { display: none; }
/* Slightly tighter vertical rhythm than a single-column modal: the point of
   the two columns is that the actions row stays reachable without scrolling. */
.page-cm .cm-modal-cols label { margin: 9px 0 4px; }
.page-cm .cm-modal-cols .cm-hint { margin-top: 3px; }
/* Two naturally-paired short fields on one row (start time + duration). */
.page-cm .cm-field-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 12px;
}
@media (max-width: 820px) {
    .page-cm .cm-modal-cols { grid-template-columns: 1fr; gap: 0; }
}
.page-cm .cm-modal {
    width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto;
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: 20px;
    padding: 26px;
    box-shadow: 0 24px 60px var(--shadow);
}
.page-cm .cm-modal h3 { font-size: 19px; margin-bottom: 16px; color: var(--text-primary); }
.page-cm .cm-modal label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin: 12px 0 5px; }
.page-cm .cm-modal input, .page-cm .cm-modal textarea, .page-cm .cm-modal select {
    width: 100%;
    padding: 10px 12px;
    border-radius: 8px;
    border: 1px solid var(--card-border);
    background: var(--card-bg);
    color: var(--text-primary);
    font-size: 14px;
    font-family: inherit;
}
.page-cm .cm-modal textarea { resize: vertical; min-height: 70px; }
/* A colour well, not a text box: the rule above stretches every input to the
   full column, which leaves a native colour swatch as a hairline. */
.page-cm .cm-modal input[type="color"] {
    width: 56px;
    height: 36px;
    padding: 3px;
    cursor: pointer;
}

/* ----- Note file picker: PDF | LaTeX -----
   One note, two ways to supply its PDF: upload a finished one, or hand over
   LaTeX that the server compiles. A segmented pair rather than a dropdown,
   because the two panes look nothing alike and the choice should be visible
   at a glance while filling the form in.

   NOT .cm-tab — that is the page's own tab bar (Notes/Exams/Classes/…) up at
   the top of this file, and reusing the name here would silently restyle it,
   since these rules come later. */
.page-cm .cm-filetabs {
    display: flex;
    gap: 6px;
    padding: 4px;
    border-radius: 10px;
    background: var(--bg-primary);
    border: 1px solid var(--card-border);
}
.page-cm .cm-filetab {
    flex: 1;
    /* 44px: a finger target, per the site-wide minimum. */
    min-height: 44px;
    border: none;
    border-radius: 7px;
    background: none;
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.page-cm .cm-filetab.active { background: var(--accent-1); color: #fff; }
.page-cm .cm-filetab:disabled { opacity: 0.45; cursor: not-allowed; }
.page-cm .cm-tabpane[hidden] { display: none; }
.page-cm .cm-tabpane > input[type="file"] { margin-top: 10px; }

/* The paste box. Monospace and tall, because this holds a whole document —
   the 70px default would show three lines of a preamble. */
.page-cm .cm-modal textarea.cm-tex {
    margin-top: 10px;
    min-height: 220px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12.5px;
    line-height: 1.5;
    white-space: pre;
    overflow-wrap: normal;
    overflow-x: auto;
}
.page-cm .cm-tex-zip {
    margin-top: 10px;
    padding: 12px 14px;
    border-radius: 10px;
    background: var(--bg-primary);
    border: 1px solid var(--card-border);
    font-size: 13px;
    line-height: 1.6;
    color: var(--text-secondary);
}
.page-cm .cm-tex-zip[hidden] { display: none; }
.page-cm .cm-tex-download { color: var(--accent-1); font-weight: 600; }
.page-cm .cm-tex-actions { margin-top: 10px; }
.page-cm .cm-tex-actions[hidden] { display: none; }

/* The compiler's own words on a failure. Scrolls in both directions inside
   its own box — a TeX log has 200-character lines and must never be allowed
   to stretch the modal. */
.page-cm .cm-tex-log {
    margin-top: 12px;
    padding: 12px 14px;
    max-height: 260px;
    overflow: auto;
    border-radius: 10px;
    background: rgba(220, 53, 69, 0.06);
    border: 1px solid rgba(220, 53, 69, 0.25);
    color: var(--text-primary);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
    line-height: 1.5;
    white-space: pre;
}
.page-cm .cm-tex-log[hidden] { display: none; }

/* ----- The side-by-side LaTeX editor -----
   A full-screen workspace opened from a note's row: source on the left, the
   compiled PDF on the right. It deliberately sits OUTSIDE .cm-overlay — this
   is a workspace, not a form, so it must not dim the page, close on an
   outside click, or scroll with the table underneath.

   No backdrop-filter anywhere in here: it covers the whole viewport, which is
   exactly the case the rest of the site avoids. --bg-primary is the one
   surface token that is fully opaque in every theme/mode pair, so the layer
   gets a real floor. */
.page-cm .tex-ed {
    position: fixed; inset: 0; z-index: 3200;
    display: flex; flex-direction: column;
    background: var(--bg-primary);
}
.page-cm .tex-ed-bar {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 10px 16px;
    border-bottom: 1px solid var(--card-border);
    background: var(--bg-secondary);
}
.page-cm .tex-ed-title {
    flex: 1 1 200px; min-width: 0;
    font-size: 14px; font-weight: 700; color: var(--text-primary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* One pill carrying the whole state of the compile, because that is the only
   thing the admin needs to glance at while typing. */
.page-cm .tex-ed-state {
    padding: 5px 12px; border-radius: 999px;
    font-size: 12px; font-weight: 700; white-space: nowrap;
    background: var(--bg-primary); color: var(--text-secondary);
    border: 1px solid var(--card-border);
}
.page-cm .tex-ed-state.busy { color: var(--accent-1); }
.page-cm .tex-ed-state.ok { color: var(--success-ink); }
.page-cm .tex-ed-state.bad { color: var(--danger-ink); }

.page-cm .tex-ed-body {
    flex: 1 1 auto; min-height: 0;
    display: flex; align-items: stretch;
}
/* The divider writes this; a percentage keeps the split when the window
   resizes, which a pixel width would not. */
.page-cm .tex-ed-code {
    flex: 0 0 var(--tex-split, 55%);
    min-width: 0; min-height: 0;
    display: flex; flex-direction: column;
}
.page-cm .tex-ed-code textarea {
    flex: 1 1 auto; min-height: 0;
    width: 100%; box-sizing: border-box;
    padding: 14px 16px;
    border: none; border-radius: 0; resize: none;
    background: var(--bg-primary); color: var(--text-primary);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 13.5px; line-height: 1.6;
    white-space: pre; overflow-wrap: normal; overflow: auto;
    tab-size: 2;
}
.page-cm .tex-ed-code textarea:focus { outline: none; }

.page-cm .tex-ed-split {
    flex: 0 0 6px; cursor: col-resize;
    background: var(--card-border);
    border-left: 1px solid var(--card-border);
    border-right: 1px solid var(--card-border);
}
.page-cm .tex-ed-split:hover { background: var(--accent-1); }
/* While dragging, the pointer must not select text or hover the panes. */
.page-cm .tex-ed.dragging { user-select: none; }
.page-cm .tex-ed.dragging .tex-ed-prev { pointer-events: none; }

.page-cm .tex-ed-prev {
    flex: 1 1 auto; min-width: 0;
    overflow: auto;
    background: var(--bg-secondary);
    padding: 16px 0 40px;
    text-align: center;
}
.page-cm .tex-ed-pg {
    margin: 0 auto 14px;
    background: #fff;                 /* paper, not a themed surface */
    box-shadow: 0 2px 12px var(--shadow);
    max-width: calc(100% - 32px);
}
.page-cm .tex-ed-pg canvas { display: block; width: 100%; height: auto; }
.page-cm .tex-ed-empty {
    padding: 40px 24px; color: var(--text-secondary);
    font-size: 13.5px; line-height: 1.7;
}

/* The engine's log, docked under the source rather than over the preview: a
   broken document is fixed by looking at the code, and the last good preview
   stays visible beside it. */
.page-cm .tex-ed-log {
    flex: 0 0 auto; max-height: 32%;
    overflow: auto; margin: 0;
    padding: 10px 16px;
    border-top: 1px solid rgba(220, 53, 69, 0.35);
    background: rgba(220, 53, 69, 0.07);
    color: var(--text-primary);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px; line-height: 1.5;
    white-space: pre; text-align: left;
}
.page-cm .tex-ed-log[hidden] { display: none; }

/* Narrow windows and phones: a side-by-side split stops being readable well
   before it stops fitting, so the two panes stack instead. Width-gated on
   purpose — this is ordinary responsive layout, not a touch-only fix. */
@media (max-width: 900px) {
    .page-cm .tex-ed-body { flex-direction: column; }
    .page-cm .tex-ed-code { flex: 1 1 50%; }
    .page-cm .tex-ed-split { display: none; }
    .page-cm .tex-ed-prev {
        flex: 1 1 50%;
        border-top: 1px solid var(--card-border);
    }
}

.page-cm .cm-modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 12px; }
.page-cm .cm-modal-delete { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--card-border); }
/* "Reuse from" — the shortcut that copies a name already used on another hub
   into this form. Tinted and boxed so it reads as a shortcut, not as one more
   field to fill in; it hides itself entirely when it has nothing to offer. */
.page-cm .cm-reuse {
    background: var(--bg-primary);
    border: 1px solid var(--card-border);
    border-radius: 10px;
    padding: 4px 12px 10px;
    margin-top: 14px;
}
.page-cm .cm-reuse[hidden] { display: none; }
.page-cm /* Access tab */
.cm-access-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; align-items: start; }
@media (max-width: 900px) {
.page-cm .cm-access-grid { grid-template-columns: 1fr; }
}
.page-cm .cm-access-col {
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: 14px;
    padding: 20px 22px;
}
.page-cm .cm-access-h { font-size: 16px; color: var(--text-primary); margin-bottom: 4px; }
.page-cm /* The two panel editors: distinct colour-coded cards (gold = online/paid, .page-cm green = offline/free) so it's always clear which panel is being edited. */
.cm-access-editor {
    border: 1px solid var(--card-border);
    border-left: 4px solid var(--card-border);
    border-radius: 12px;
    background: var(--card-bg);
    padding: 2px 16px 16px;
    margin-top: 18px;
}
.page-cm .cm-access-editor--online { border-left-color: var(--gold-2, #e6a817); }
.page-cm .cm-access-editor--offline { border-left-color: var(--success, #10b981); }
/* The pre-launch lock switch. Sits above both panel editors because it decides
   whether users see either of them at all; when it's on they're dimmed. */
.page-cm .cm-access-lock {
    border: 1px solid var(--card-border);
    border-left: 4px solid var(--card-border);
    border-radius: 12px;
    background: var(--card-bg);
    padding: 14px 16px 16px;
    margin: 16px 0 4px;
}
.page-cm .cm-access-lock.is-on { border-left-color: var(--danger, #d32f2f); }
.page-cm .cm-access-lock-toggle {
    display: flex; align-items: center; gap: 9px; cursor: pointer;
    font-size: 13.5px; font-weight: 700; color: var(--text-primary);
}
.page-cm .cm-access-lock-toggle input { width: 16px; height: 16px; cursor: pointer; }
.page-cm textarea.cm-pinput { resize: vertical; min-height: 62px; }
.page-cm .cm-access-editor.is-muted { opacity: 0.5; }
.page-cm .cm-access-editor-head {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    margin: 14px 0 2px;
    font-size: 13.5px; font-weight: 700; color: var(--text-primary);
}
.page-cm .cm-editor-chip {
    font-size: 10px; font-weight: 800; letter-spacing: 0.5px; text-transform: uppercase;
    padding: 2px 9px; border-radius: 20px;
}
.page-cm .cm-access-editor--online .cm-editor-chip {
    background: linear-gradient(135deg, var(--gold-1, #f5c518), var(--gold-2, #e6a817));
    color: var(--gold-ink, #4a3403);
}
.page-cm .cm-access-editor--offline .cm-editor-chip { background: var(--success, #10b981); color: #fff; }
.page-cm .cm-duration-row { display: flex; gap: 8px; align-items: center; }
.page-cm .cm-duration-row select { max-width: 180px; }
.page-cm .cm-duration-row input { max-width: 110px; }
.page-cm .cm-req-count {
    display: inline-block; min-width: 20px; padding: 1px 8px; border-radius: 20px;
    background: #d32f2f; color: #fff; font-size: 12px; font-weight: 700; vertical-align: middle;
}
.page-cm .cm-req-list { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
.page-cm .cm-req-row {
    background: var(--card-bg); border: 1px solid var(--card-border);
    border-radius: 10px; padding: 10px 12px;
}
.page-cm .cm-req-row .who { font-weight: 600; font-size: 14px; }
.page-cm .cm-req-row .meta { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
.page-cm .cm-req-row .req-note {
    font-size: 12.5px; color: var(--text-primary); margin-top: 6px;
    background: var(--bg-secondary); border-radius: 7px; padding: 6px 9px;
    white-space: pre-wrap; word-break: break-word;
}
.page-cm .cm-req-controls { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.page-cm .cm-req-empty { font-size: 13px; color: var(--text-secondary); margin-top: 10px; }
.page-cm .cm-plabel { display: block; font-size: 12.5px; font-weight: 600; color: var(--text-secondary); margin: 12px 0 5px; }
.page-cm .cm-pinput {
    width: 100%;
    padding: 9px 12px;
    border-radius: 8px;
    border: 1px solid var(--card-border);
    background: var(--card-bg);
    color: var(--text-primary);
    font-size: 14px;
    font-family: inherit;
}
.page-cm .cm-fmt-bar {
    display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
    padding: 8px 10px; margin: 8px 0 4px;
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: 10px;
}
.page-cm .cm-fmt-btn {
    min-width: 28px; height: 26px; padding: 0 7px;
    border: 1px solid var(--card-border); border-radius: 7px;
    background: var(--bg-secondary); color: var(--text-primary);
    font-size: 13px; cursor: pointer; line-height: 1;
}
.page-cm .cm-fmt-btn:hover, .page-cm .cm-fmt-swatch:hover { border-color: var(--text-secondary); }
.page-cm .cm-fmt-swatch {
    width: 20px; height: 20px; border-radius: 50%;
    border: 2px solid var(--card-border); cursor: pointer; padding: 0;
}
.page-cm .cm-fmt-custom input[type="color"] {
    width: 28px; height: 26px; padding: 1px 2px;
    border: 1px solid var(--card-border); border-radius: 7px;
    background: var(--bg-secondary); cursor: pointer;
}
.page-cm .cm-fmt-sep { width: 1px; height: 18px; background: var(--card-border); }
.page-cm .cm-features { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
.page-cm .cm-feature-row {
    display: flex; gap: 8px; align-items: flex-start;
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: 10px;
    padding: 8px;
}
.page-cm .cm-feature-fields { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.page-cm .cm-feature-fields input { padding: 7px 10px; border-radius: 7px; border: 1px solid var(--card-border); background: var(--bg-secondary); color: var(--text-primary); font-size: 13px; }
.page-cm .cm-feature-fields input.sub { font-size: 12px; color: var(--text-secondary); }
.page-cm .cm-feature-ctrls { display: flex; flex-direction: column; gap: 4px; }
.page-cm .cm-feature-ctrls button {
    width: 26px; height: 22px;
    border: 1px solid var(--card-border);
    background: var(--bg-secondary);
    color: var(--text-secondary);
    border-radius: 6px; cursor: pointer; font-size: 12px; line-height: 1;
}
.page-cm .cm-feature-ctrls button.rm { color: #d32f2f; }
.page-cm .cm-plan-results { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
.page-cm .cm-plan-row {
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: 10px;
    padding: 10px 12px;
}
.page-cm .cm-plan-row .who { font-weight: 600; font-size: 14px; }
.page-cm .cm-plan-row .meta { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
.page-cm .cm-plan-controls { display: flex; gap: 8px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
.page-cm .cm-plan-controls input[type="date"] {
    padding: 6px 8px; border-radius: 7px; border: 1px solid var(--card-border);
    background: var(--bg-secondary); color: var(--text-primary); font-size: 12.5px;
}
.page-cm .cm-plan-badge { display:inline-block; padding: 2px 9px; border-radius: 20px; font-size: 11px; font-weight: 700; }
.page-cm .cm-plan-badge.access { background: linear-gradient(135deg, #f5c518, #e6a817); color: #4a3403; }
.page-cm .cm-plan-badge.free { background: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--card-border); }
.page-cm .cm-grant-list { list-style: none; padding: 0; margin: 10px 0; }
.page-cm .cm-grant-list li {
    display: flex; justify-content: space-between; align-items: center;
    padding: 8px 10px; border-bottom: 1px solid var(--card-border); font-size: 14px;
}
.page-cm .cm-grant-add { display: flex; gap: 8px; margin-top: 12px; }
.page-cm .cm-hint { font-size: 12.5px; color: var(--text-secondary); margin-top: 4px; }
/* Verdict panel under a user row after a refund check. Green/red match the
   form-message convention used on the profile and auth pages. */
.page-cm .cm-refund-verdict {
    margin-top: 10px;
    padding: 10px 12px;
    border-radius: 9px;
    font-size: 13px;
    line-height: 1.5;
}
.page-cm .cm-refund-verdict strong { display: block; margin-bottom: 3px; }
.page-cm .cm-refund-verdict.ok {
    background: rgba(76, 175, 80, 0.10);
    border: 1px solid rgba(76, 175, 80, 0.30);
}
.page-cm .cm-refund-verdict.no {
    background: rgba(244, 67, 54, 0.08);
    border: 1px solid rgba(244, 67, 54, 0.28);
}
.page-cm /* Bulk actions bar */
.cm-bulkbar {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: 10px;
    padding: 10px 14px;
    margin-bottom: 14px;
    font-size: 13px;
}
.page-cm .cm-bulkbar select {
    padding: 7px 10px;
    border-radius: 8px;
    border: 1px solid var(--card-border);
    background: var(--card-bg);
    color: var(--text-primary);
    font-size: 13px;
}
.page-cm td .cm-note-check, .page-cm th #bulkAll { width: 15px; height: 15px; cursor: pointer; }
.page-cm /* Staged edits: rows changed but not yet written, .page-cm and the bar that writes them */
tr.cm-dirty td, .page-cm .cm-list-row.cm-dirty { background: rgba(230, 168, 23, 0.12); }
.page-cm tr.cm-dirty td:first-child { box-shadow: inset 3px 0 0 #e6a817; }
.page-cm .cm-list-row.cm-dirty { border-color: rgba(230, 168, 23, 0.45); }
.page-cm .cm-savebar {
    position: fixed;
    left: 50%;
    bottom: 24px;
    z-index: 2500;
    display: flex;
    align-items: center;
    gap: 14px;
    max-width: min(760px, calc(100vw - 32px));
    padding: 12px 16px;
    border-radius: 14px;
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    box-shadow: 0 18px 44px var(--shadow);
    font-size: 13px;
    opacity: 0;
    pointer-events: none;
    transform: translate(-50%, 150%);
    transition: transform var(--dur-fast) var(--ease-standard), opacity var(--dur-fast) var(--ease-standard);
}
.page-cm .cm-savebar.open { opacity: 1; pointer-events: auto; transform: translate(-50%, 0); }
.page-cm .cm-savebar-text { min-width: 0; }
.page-cm .cm-savebar strong { display: block; color: var(--text-primary); }
.page-cm .cm-savebar-list {
    display: block;
    margin-top: 2px;
    max-width: 420px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12px;
    color: var(--text-secondary);
}
.page-cm /* Sections & tags */
.cm-sec-row td {
    background: var(--bg-secondary);
    font-weight: 700;
    font-size: 13px;
    color: var(--text-primary);
    padding: 9px 14px;
}
.page-cm .cm-sec-row .count { font-weight: 400; color: var(--text-secondary); margin-left: 8px; font-size: 12px; }
.page-cm .cm-tag {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 20px;
    border: 1px solid;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    margin-left: 6px;
    vertical-align: 1px;
    white-space: nowrap;
}
.page-cm /* Drag-and-drop */
.cm-drag-handle {
    cursor: grab;
    color: var(--text-secondary);
    font-size: 15px;
    user-select: none;
    text-align: center;
}
.page-cm .cm-dragging { opacity: 0.45; }
.page-cm /* Draggable list rows (sections modal, .page-cm subject reorder modal) */
.cm-list { display: flex; flex-direction: column; gap: 8px; margin: 12px 0; }
.page-cm .cm-list-row {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: 10px;
    padding: 9px 12px;
    font-size: 14px;
}
.page-cm .cm-list-row .grow { flex: 1; min-width: 0; }
.page-cm .cm-list-row .title { font-weight: 600; color: var(--text-primary); }
.page-cm .cm-list-row .cm-btn { padding: 5px 10px; font-size: 12px; }
.page-cm /* Tag manager rows */
.cm-tag-row { display: flex; align-items: center; gap: 8px; }
.page-cm .cm-tag-row input[type="color"] {
    width: 40px; height: 34px; padding: 2px;
    border: 1px solid var(--card-border); border-radius: 8px;
    background: var(--card-bg); cursor: pointer;
}
.page-cm .cm-tag-row input[type="text"] { flex: 1; }

/* Announcements tab. The colour dot borrows the .ann--<tone> classes, so a
   swatch in the table is always the exact colour the notice will use; the
   preview holds a REAL .ann node built by announcements.js. Both therefore
   follow the component instead of imitating it. */
.page-cm .cm-ann-dot {
    display: inline-block;
    width: 11px;
    height: 11px;
    border-radius: 50%;
    background: var(--ann-solid);
    vertical-align: -1px;
}
.page-cm .cm-ann-preview {
    margin-top: 6px;
    padding: 16px;
    border: 1px dashed var(--card-border);
    border-radius: 12px;
    background: var(--bg-secondary);
}
.page-cm .cm-ann-preview .ann-stack { width: 100%; margin: 0; }
.page-cm /* Subject icon picker (icons from icons.js) */
.cm-icon-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 8px;
    margin-top: 6px;
}
.page-cm .cm-icon-grid button {
    aspect-ratio: 1;
    border: 2px solid var(--card-border);
    border-radius: 10px;
    background: var(--card-bg);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: border-color var(--dur-fast) var(--ease-standard), transform var(--dur-fast) var(--ease-standard);
}
.page-cm .cm-icon-grid button:hover { transform: scale(1.06); border-color: var(--accent-1); }
.page-cm .cm-icon-grid button.selected {
    border-color: var(--accent-1);
    background: rgba(99, 102, 241, 0.1);
}
.page-cm .cm-icon-grid svg { width: 22px; height: 22px; stroke: var(--text-primary); }
.page-cm .cm-icon-grid button.selected svg { stroke: var(--accent-1); }
.page-cm /* Tag checkboxes inside the section edit form */
.cm-checks { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.page-cm .cm-checks label {
    display: inline-flex; align-items: center; gap: 6px;
    margin: 0; padding: 6px 10px;
    border: 1px solid var(--card-border); border-radius: 8px;
    background: var(--card-bg); cursor: pointer;
    font-size: 13px; font-weight: 500; color: var(--text-primary);
}
.page-cm .cm-checks input { width: auto; }
.page-cm /* Avatars panel */
.cm-avatar-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 16px;
}
.page-cm .cm-avatar-card {
    border: 1px solid var(--card-border);
    border-radius: 12px;
    background: var(--card-bg);
    padding: 14px 10px 10px;
    text-align: center;
}
.page-cm .cm-avatar-card img {
    width: 84px; height: 84px;
    border-radius: 50%;
    object-fit: cover;
    display: block;
    margin: 0 auto 10px;
    border: 2px solid var(--card-border);
}
.page-cm /* A hidden avatar still shows, .page-cm dimmed — the admin needs to see what they've retired. */
.cm-avatar-card.hidden img { opacity: 0.35; filter: grayscale(1); }
.page-cm .cm-avatar-name {
    font-size: 13px; font-weight: 600; color: var(--text-primary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.page-cm .cm-avatar-meta { font-size: 11.5px; color: var(--text-secondary); margin-top: 2px; }
.page-cm .cm-avatar-actions { display: flex; gap: 6px; justify-content: center; margin-top: 10px; }
.page-cm .cm-avatar-actions button {
    padding: 4px 10px; font-size: 12px; font-weight: 600;
    border-radius: 7px; border: 1px solid var(--card-border);
    background: var(--bg-primary); color: var(--text-primary);
    cursor: pointer; font-family: inherit;
}
.page-cm .cm-avatar-actions button:hover { border-color: var(--accent-1); color: var(--accent-1); }
.page-cm .cm-avatar-actions button.danger:hover { border-color: #ef4444; color: #ef4444; }
.page-cm .cm-avatar-actions button:disabled { opacity: 0.4; cursor: not-allowed; }
.page-cm .cm-avatar-preview {
    width: 120px; height: 120px; border-radius: 50%; object-fit: cover;
    display: block; margin: 10px auto; border: 2px solid var(--card-border);
}
.page-cm /* Exam question editor (the Questions sub-view of the Exams tab) */
.cm-modal.wide { max-width: 780px; }
.page-cm .cm-qhead { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 6px; }
.page-cm .cm-qhead h2 { font-size: 19px; color: var(--text-primary); flex: 1; min-width: 200px; }
.page-cm .cm-q-row { align-items: flex-start; }
.page-cm .cm-q-no {
    flex: 0 0 auto; font-weight: 700; font-size: 13px; color: var(--text-secondary);
    padding-top: 2px; min-width: 34px;
}
/* pre-line for the same reason the exam page uses it — the owner must see a
   question laid out the way a student will. */
.page-cm .cm-q-text { font-size: 14px; color: var(--text-primary); overflow-wrap: anywhere; white-space: pre-line; }
.page-cm .cm-q-meta { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }
.page-cm .cm-q-correct {
    display: inline-block; padding: 1px 8px; border-radius: 20px;
    background: rgba(76, 175, 80, 0.15); color: #388e3c;
    font-size: 11px; font-weight: 700;
}
/* A RETIRED question: out of the live paper, kept for the results that
   already contain it. Dimmed rather than hidden — the admin has to be able
   to see it in order to put it back. */
.page-cm .cm-q-retired { opacity: 0.6; }
.page-cm .cm-q-retired .cm-q-text { text-decoration: line-through; }
.page-cm .cm-q-chip {
    display: inline-block; padding: 1px 8px; margin-right: 6px; border-radius: 20px;
    background: rgba(127, 127, 127, 0.22); color: var(--text-secondary);
    font-size: 11px; font-weight: 700;
}
.page-cm /* Option rows inside the question modal */
.cm-opt-wrap { margin-bottom: 8px; }
.page-cm .cm-opt-row { display: flex; align-items: center; gap: 8px; }
.page-cm .cm-opt-wrap .cm-math-preview {
    margin: 4px 38px 0 24px; padding: 6px 10px; font-size: 13.5px;
}
.page-cm .cm-opt-row input[type="radio"] { width: 16px; height: 16px; flex: 0 0 auto; cursor: pointer; }
.page-cm .cm-opt-row input[type="text"] { flex: 1; }
.page-cm .cm-opt-row button {
    flex: 0 0 auto; width: 30px; height: 34px;
    border: 1px solid var(--card-border); border-radius: 8px;
    background: var(--card-bg); color: #d32f2f; cursor: pointer; font-size: 13px;
}
.page-cm .cm-opt-row button:disabled { opacity: 0.35; cursor: not-allowed; }
.page-cm .cm-math-preview {
    border: 1px dashed var(--card-border); border-radius: 8px;
    background: var(--card-bg); padding: 10px 12px; margin-top: 6px;
    font-size: 14px; color: var(--text-primary); min-height: 20px;
    overflow-x: auto;
}
.page-cm /* Bulk "Add multiple questions" paste modal: example + parsed preview */
.cm-bulk-example {
    border: 1px dashed var(--card-border); border-radius: 8px;
    background: var(--card-bg); padding: 10px 12px; margin-top: 6px;
    font-size: 12.5px; color: var(--text-secondary); white-space: pre-wrap;
    overflow-x: auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.page-cm .cm-bulk-preview { margin-top: 12px; }
.page-cm .cm-bulk-errors {
    border: 1px solid var(--danger, #d64545); border-radius: 10px;
    background: color-mix(in srgb, var(--danger, #d64545) 10%, transparent);
    padding: 10px 14px; margin-bottom: 12px; font-size: 13px; color: var(--text-primary);
}
.page-cm .cm-bulk-errors ul { margin: 6px 0 0; padding-left: 20px; }
.page-cm .cm-bulk-errors li { margin: 2px 0; }
.page-cm .cm-bulk-q {
    border: 1px solid var(--card-border); border-radius: 12px;
    background: var(--card-bg); padding: 12px 14px; margin-bottom: 10px;
}
.page-cm .cm-bulk-q .cm-q-no { margin-bottom: 4px; }
.page-cm .cm-bulk-opts { margin: 8px 0 0; padding-left: 22px; }
.page-cm .cm-bulk-opts li { font-size: 13.5px; color: var(--text-primary); margin: 3px 0; }
.page-cm .cm-bulk-opts li.correct { color: var(--text-primary); font-weight: 600; }
.page-cm .cm-bulk-badge {
    margin-left: 8px; font-size: 11px; font-weight: 700; color: var(--accent-1);
}
.page-cm .cm-bulk-expl {
    margin-top: 8px; font-size: 13px; color: var(--text-secondary);
    border-top: 1px solid var(--card-border); padding-top: 8px;
}
.page-cm /* Analytics modal: stat tiles + single-hue accuracy bars (site accent) */
.cm-stats-grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 10px; margin: 12px 0 18px;
}
.page-cm .cm-stat {
    background: var(--card-bg); border: 1px solid var(--card-border);
    border-radius: 12px; padding: 12px 14px;
}
.page-cm .cm-stat .num { font-size: 21px; font-weight: 800; color: var(--text-primary); }
.page-cm .cm-stat .lab { font-size: 11.5px; color: var(--text-secondary); margin-top: 2px; }
.page-cm .cm-accrow { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; }
.page-cm .cm-accrow .q { flex: 0 0 34px; font-size: 12px; font-weight: 700; color: var(--text-secondary); }
.page-cm .cm-accrow .t {
    flex: 1; min-width: 0; font-size: 12.5px; color: var(--text-primary);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.page-cm .cm-accbar {
    flex: 0 0 140px; height: 8px; border-radius: 4px;
    background: var(--bg-secondary); border: 1px solid var(--card-border);
    overflow: hidden;
}
.page-cm .cm-accbar i { display: block; height: 100%; background: var(--accent-1); border-radius: 4px; }
.page-cm .cm-accrow .v { flex: 0 0 76px; font-size: 12px; color: var(--text-secondary); text-align: right; }

/* ===== PAGE: dashboard.html (scoped .page-dashboard; moved verbatim from its <style> block) ===== */
.page-dashboard /* Profile row: the avatar sits beside the greeting rather than above it. */
.dash-hero-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 30px;
  text-align: left;
  flex-wrap: wrap;
}
.page-dashboard .dash-hero-text {
  max-width: 440px;
  /* Lift the greeting so the heading sits on the avatar's vertical middle;
     the tightened line gaps keep the three lines reading as one block. */
  margin-top: -52px;
}
.page-dashboard .dash-hero-text .hero-badge { margin-bottom: 10px; }
.page-dashboard .dash-hero-text h1 { margin-bottom: 6px; }
.page-dashboard .dash-hero-text p { margin: 0; }
.page-dashboard .dash-hero-avatar {
  flex: none;
  width: 132px;
  height: 132px;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  color: var(--text-on-accent);
  font-size: 52px;
  font-weight: 700;
  /* Accent ring as a real border (not a box-shadow) so it sits flush against
     the clipped avatar with no hairline seam; plus a soft lift shadow. */
  border: 3px solid var(--accent-1);
  box-shadow: 0 12px 30px var(--shadow);
  overflow: hidden;
}
.page-dashboard .dash-hero-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
@media (max-width: 600px) {
.page-dashboard .dash-hero-row { flex-direction: column; text-align: center; gap: 16px; }
.page-dashboard .dash-hero-text { text-align: center; margin-top: 0; max-width: none; }
}
.page-dashboard .dash-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 20px;
}
.page-dashboard .dash-stat-card {
  padding: 20px;
  text-align: center;
}
.page-dashboard .dash-stat-value {
  font-size: 32px;
  font-weight: 700;
  color: var(--accent-1);
}
.page-dashboard .dash-stat-label {
  font-size: 13px;
  color: var(--text-secondary);
  text-transform: uppercase;
  margin-top: 4px;
}
.page-dashboard .dash-grid-2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 30px;
}
.page-dashboard .dash-card h3 {
  margin-bottom: 16px;
  border-bottom: 1px solid var(--card-border);
  padding-bottom: 12px;
}
.page-dashboard .recent-note-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 0;
  border-bottom: 1px solid var(--card-border);
  gap: 12px;
}
.page-dashboard .recent-note-row:last-child { border-bottom: none; }
.page-dashboard .recent-note-title { font-weight: 600; color: var(--text-primary); }
.page-dashboard .recent-note-time { font-size: 12px; color: var(--text-secondary); white-space: nowrap; }
.page-dashboard .empty-state {
  padding: 24px 0;
  text-align: center;
  color: var(--text-secondary);
}
.page-dashboard .coming-soon-badge {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  background: var(--card-border);
  color: var(--text-secondary);
  padding: 3px 8px;
  border-radius: 6px;
  margin-left: 8px;
}

/* ===== PAGE: profile.html (scoped .page-profile; moved verbatim from its <style> block) ===== */
.page-profile .profile-container {
    max-width: 1000px;
    /* Top margin clears the fixed navbar */
    margin: 120px auto 40px;
    padding: 0 20px;
    display: grid;
    grid-template-columns: 1fr 2fr;
    gap: 30px;
}
@media (max-width: 768px) {
.page-profile .profile-container {
        grid-template-columns: 1fr;
    }
}
.page-profile .profile-card {
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: 20px;
    padding: 28px;
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    box-shadow: 0 12px 32px var(--shadow);
}
.page-profile .profile-header {
    text-align: center;
    margin-bottom: 24px;
}
.page-profile .profile-picture-wrapper {
    position: relative;
    width: 120px;
    height: 120px;
    margin: 0 auto 20px;
    border-radius: 50%;
    overflow: hidden;
    background-color: var(--card-border);
    /* Accent ring as a real border (not a box-shadow) so it sits flush against
       the clipped avatar with no hairline seam. Matches the dashboard hero. */
    border: 3px solid var(--accent-1);
}
.page-profile .profile-picture {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.page-profile /* Letter avatar shown when no photo is set */
.letter-avatar {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 52px;
    font-weight: 800;
    color: #fff;
    background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
    user-select: none;
}
.page-profile /* The Change and Remove buttons live below the avatar (see .picture-actions). */
/* ===== Avatar picker: a centred modal, .page-profile not a popover under the picture =====
   No backdrop-filter on the scrim: it covers the whole viewport, .page-profile and the hero
   blobs animate behind it (see CLAUDE.md -- this is what made the premium modal
   feel dead on weak GPUs). A darker plain scrim separates it just as well. */
.avatar-overlay {
    position: fixed;
    inset: 0;
    z-index: 3000;
    background: var(--scrim-strong);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
}
.page-profile .avatar-modal {
    width: 100%;
    max-width: 520px;
    max-height: calc(100vh - 48px);
    overflow-y: auto;
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: 20px;
    padding: 26px;
    box-shadow: 0 24px 60px var(--shadow);
    text-align: center;
}
.page-profile .avatar-modal h3 { font-size: 20px; margin-bottom: 6px; color: var(--text-primary); }
.page-profile .avatar-modal .avatar-modal-sub {
    font-size: 14px;
    color: var(--text-secondary);
    margin-bottom: 20px;
}
.page-profile .avatar-picker {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 14px;
    margin-bottom: 22px;
    /* The set is long enough to overflow the modal; scroll the grid, not the page. */
    max-height: 46vh;
    overflow-y: auto;
    padding: 4px;
}
.page-profile .avatar-option {
    padding: 0;
    border: 2px solid var(--card-border);
    border-radius: 50%;
    background: none;
    cursor: pointer;
    overflow: hidden;
    aspect-ratio: 1;
    transition: transform var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard);
}
.page-profile .avatar-option img { width: 100%; height: 100%; display: block; object-fit: cover; }
.page-profile .avatar-option:hover { transform: scale(1.08); border-color: var(--accent-1); }
.page-profile .avatar-option:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 2px; }
.page-profile .avatar-option.selected { border-color: var(--accent-1); box-shadow: 0 0 0 3px var(--accent-1); }
.page-profile .avatar-modal-actions { display: flex; justify-content: center; }
.page-profile .avatar-modal-actions button {
    padding: 10px 22px;
    border-radius: 10px;
    border: 1px solid var(--card-border);
    background: var(--bg-primary);
    color: var(--text-primary);
    font-size: 14px;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
}
.page-profile .avatar-modal-actions button:hover { border-color: var(--accent-1); color: var(--accent-1); }
@media (max-width: 520px) {
.page-profile .avatar-picker { grid-template-columns: repeat(4, 1fr); gap: 10px; }
}
.page-profile .picture-actions {
    display: flex;
    gap: 8px;
    justify-content: center;
    margin-bottom: 14px;
}
.page-profile .picture-actions button {
    padding: 6px 14px;
    border-radius: 8px;
    border: none;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.page-profile .picture-actions .pic-change { background: var(--accent-1); color: #fff; }
.page-profile .picture-actions .pic-change:hover { filter: brightness(1.05); }
.page-profile .picture-actions .pic-save { background: var(--accent-1); color: #fff; }
.page-profile .picture-actions .pic-cancel { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--card-border); }
.page-profile .picture-actions .pic-remove { background: transparent; color: #d32f2f; border: 1px solid rgba(211,47,47,0.35); }
.page-profile .picture-hint {
    font-size: 12px;
    color: var(--text-secondary);
    text-align: center;
    margin-bottom: 12px;
}
/* Password show/hide. The comment used to sit BETWEEN `.page-profile` and
   `.password-input-wrap`, which does not split a selector — the two joined
   into a descendant selector and the rule silently scoped to profile only.
   Written out explicitly here; the parsed result is unchanged. */
.page-profile .password-input-wrap { position: relative; }
/* Login's reveal toggle. Login has no page-class, so this scopes to the
   .login-page layout wrapper. Mirrors the reset page's toggle placement;
   the shared password-toggle.js injects the eye and wires the click. */
.login-page .password-input-wrap { position: relative; }
.login-page .password-input-wrap input { width: 100%; padding-right: 48px; }
.login-page .toggle-password {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  padding: 4px;
}
.page-profile .password-input-wrap input { width: 100%; padding-right: 54px; }
.page-profile .toggle-password {
    position: absolute;
    right: 12px;
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-secondary);
    font-size: 13px;
    font-weight: 600;
}
.page-profile .profile-name {
    font-size: 24px;
    font-weight: 700;
    margin-bottom: 4px;
    color: var(--text-primary);
}
.page-profile .profile-role {
    font-size: 14px;
    color: var(--accent-1);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
}
.page-profile .profile-stats {
    list-style: none;
    padding: 0;
    margin: 0;
}
.page-profile .profile-stats li {
    display: flex;
    justify-content: space-between;
    padding: 12px 0;
    border-bottom: 1px solid var(--card-border);
    font-size: 14px;
}
.page-profile .profile-stats li:last-child {
    border-bottom: none;
}
.page-profile .stat-label {
    color: var(--text-secondary);
    font-weight: 600;
}
.page-profile .stat-value {
    color: var(--text-primary);
    text-align: right;
}
.page-profile .badge {
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 600;
}
.page-profile .badge-success { background: rgba(79, 156, 91, 0.16); color: var(--accent-1); }
.page-profile .badge-warning { background: rgba(255, 152, 0, 0.12); color: #c77700; }
.page-profile .badge-info { background: rgba(79, 156, 91, 0.12); color: var(--accent-1); border: 1px solid rgba(79, 156, 91, 0.25); }
.page-profile [data-theme="dark"] .badge-warning { color: #ffb74d; }
.page-profile .settings-section {
    margin-bottom: 30px;
}
.page-profile .settings-section h3 {
    margin-bottom: 16px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--card-border);
    color: var(--text-primary);
}
.page-profile .form-message {
    padding: 12px;
    border-radius: 8px;
    margin-bottom: 16px;
    font-size: 14px;
    display: none;
}
.page-profile .form-message.error {
    background-color: rgba(244, 67, 54, 0.1);
    color: #d32f2f;
    border: 1px solid rgba(244, 67, 54, 0.3);
    display: block;
}
.page-profile .form-message.success {
    background-color: rgba(76, 175, 80, 0.1);
    color: #388e3c;
    border: 1px solid rgba(76, 175, 80, 0.3);
    display: block;
}
.page-profile .danger-zone {
    border: 1px solid rgba(211, 47, 47, 0.3);
    border-radius: 8px;
    padding: 16px;
    margin-bottom: 0;
}

/* ===== PAGE: register.html (scoped .page-register; moved verbatim from its <style> block) ===== */
.page-register /* Register specific styles */
.password-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}
/* (deliberately no .password-wrapper input padding rule: the page's old
   inline block declared padding-right: 40px, but at (0,1,1) it always lost
   to .login-page .form-group input (0,2,1) — the declaration never applied.
   Scoping would have resurrected it and shrunk the text area; the shipped
   look keeps the main sheet's 16px padding.) */
.page-register .toggle-password {
  position: absolute;
  right: 12px;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 16px;
  color: var(--text-secondary);
  padding: 4px;
}
.page-register .password-strength {
  display: flex;
  height: 4px;
  margin-top: 8px;
  border-radius: 2px;
  background-color: var(--border-color);
  overflow: hidden;
}
.page-register .strength-bar {
  height: 100%;
  width: 0%;
  transition: width var(--dur-base) var(--ease-standard), background-color var(--dur-base) var(--ease-standard);
}
.page-register .strength-text {
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: 4px;
  text-align: right;
  height: 14px;
}
.page-register .strength-weak { background-color: #f44336; width: 33%; }
.page-register .strength-medium { background-color: #ff9800; width: 66%; }
.page-register .strength-strong { background-color: #4caf50; width: 100%; }
.page-register .form-message {
  padding: 12px;
  border-radius: 8px;
  margin-bottom: 16px;
  font-size: 14px;
  display: none;
}
.page-register .form-message.error {
  background-color: rgba(244, 67, 54, 0.1);
  color: #d32f2f;
  border: 1px solid rgba(244, 67, 54, 0.3);
  display: block;
}
.page-register .form-message.success {
  background-color: rgba(76, 175, 80, 0.1);
  color: #388e3c;
  border: 1px solid rgba(76, 175, 80, 0.3);
  display: block;
}
.page-register .input-error {
  border-color: #f44336 !important;
}
.page-register .error-text {
  color: #f44336;
  font-size: 12px;
  margin-top: 4px;
  display: none;
}
.page-register .input-error + .error-text, .page-register .input-error ~ .error-text {
  display: block;
}
.page-register .btn-loading {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}
.page-register .btn-loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 20px;
  margin-top: -10px;
  margin-left: -10px;
  border: 2px solid rgba(255,255,255,0.3);
  border-top-color: white;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

/* ===== PAGE: reset-password.html (scoped .page-reset; moved verbatim from its <style> block) ===== */
.page-reset /* Password Strength Styles */
.password-strength {
  display: flex;
  gap: 4px;
  margin-top: 8px;
  height: 4px;
}
.page-reset .strength-bar {
  flex: 1;
  background-color: var(--card-border);
  border-radius: 2px;
  transition: var(--transition);
}
.page-reset .strength-text {
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: 4px;
  text-align: right;
}
.page-reset .strength-weak .strength-bar:nth-child(1) { background-color: #f44336; }
.page-reset .strength-medium .strength-bar:nth-child(1), .page-reset .strength-medium .strength-bar:nth-child(2) { background-color: #ff9800; }
.page-reset .strength-strong .strength-bar { background-color: #4caf50; }
.page-reset .password-input-wrap {
  position: relative;
}
.page-reset .toggle-password {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-secondary);
  font-size: 14px;
}

/* ===== PAGE: viewer.html (scoped .page-viewer; moved verbatim from its <style> block) ===== */
.page-viewer /* ===== In-site PDF viewer (PDF.js renders into canvases) ===== */
  .vw-wrap {
    max-width: 1080px;
    margin: 0 auto;
    padding: 0 16px 60px;
  }
.page-viewer .vw-breadcrumb { padding-top: 92px; padding-left: 16px; margin-bottom: 14px; }
.page-viewer .viewer-shell {
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: 16px;
    overflow: hidden;
  }
.page-viewer /* In fullscreen the shell itself becomes the scrolling document. */
  .viewer-shell:fullscreen {
    border: none;
    border-radius: 0;
    overflow-y: auto;
  }
.page-viewer .vw-toolbar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding: 10px 16px;
    background: var(--card-bg);
    border-bottom: 1px solid var(--card-border);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }
.page-viewer .vw-title {
    font-weight: 700;
    font-size: 14.5px;
    color: var(--text-primary);
    letter-spacing: -0.01em;
    margin-right: auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 46%;
  }
.page-viewer .vw-group { display: flex; align-items: center; gap: 6px; }
.page-viewer .vw-btn {
    border: 1px solid var(--card-border);
    background: var(--bg-secondary);
    color: var(--text-primary);
    border-radius: 8px;
    font-size: 13px;
    font-weight: 600;
    padding: 6px 10px;
    cursor: pointer;
    line-height: 1.2;
    transition: var(--transition);
  }
.page-viewer .vw-btn:hover { border-color: var(--accent-1); color: var(--accent-1); }
.page-viewer .vw-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.page-viewer .vw-btn.done-active {
    background: rgba(16, 185, 129, 0.14);
    border-color: rgba(16, 185, 129, 0.5);
    color: #059669;
  }
/* The floating full-screen toggle that stands in for the whole toolbar once
   it slides away on a phone. Hidden everywhere by default — only the touch
   block below reveals it, and only while the bar is collapsed.

   `.vw-btn.vw-fs-float`, not `.vw-fs-float`: it carries `vw-btn` too, and the
   `max-width: 640px` block further down sets `.page-viewer .vw-btn` to
   `display: inline-flex` at the very same (0,2,0) specificity but later in
   the file. A one-class selector loses that tie, which left this button
   sitting in the flow on every narrow screen — visible on a narrowed desktop
   where it must never appear, and, because collapsing then moved it to
   `fixed`, pulling 44px out of the document and restarting the scroll
   feedback loop the transform above exists to avoid. */
.page-viewer .vw-btn.vw-fs-float { display: none; }
.page-viewer .vw-page-group { font-size: 13px; color: var(--text-secondary); }
.page-viewer .vw-page-input {
    width: 48px;
    text-align: center;
    padding: 5px 4px;
    border: 1px solid var(--card-border);
    border-radius: 8px;
    background: var(--bg-secondary);
    color: var(--text-primary);
    font-size: 13px;
  }
.page-viewer .vw-zoom-label { font-size: 12.5px; color: var(--text-secondary); min-width: 40px; text-align: center; }
.page-viewer /* The rendered pages */
  #pdfPages {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 18px;
    padding: 22px 12px 40px;
    min-height: 320px;
  }
.page-viewer .pdf-page {
    background: #ffffff;
    box-shadow: 0 4px 22px var(--shadow);
    border-radius: 4px;
    position: relative;
  }
.page-viewer .pdf-page canvas { display: block; width: 100%; height: 100%; border-radius: 4px; }
.page-viewer .pdf-page .pdf-page-num {
    position: absolute;
    bottom: -16px;
    right: 2px;
    font-size: 10.5px;
    color: var(--text-secondary);
    opacity: 0.7;
  }
.page-viewer /* Loading / locked / error panel */
  .vw-status {
    text-align: center;
    padding: 70px 24px;
    color: var(--text-secondary);
    font-size: 15px;
    line-height: 1.7;
  }
.page-viewer .vw-status .vw-status-icon { font-size: 40px; display: block; margin-bottom: 14px; }
.page-viewer .vw-status .btn { margin-top: 18px; }
.page-viewer .vw-status a.vw-cta, .page-viewer .vw-status button.vw-cta {
    display: inline-block;
    margin-top: 18px;
    padding: 11px 22px;
    border: none;
    border-radius: 12px;
    background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
    color: #fff;
    font-weight: 600;
    font-size: 14px;
    cursor: pointer;
    text-decoration: none;
  }
.page-viewer /* "Up next" card after the last page (viewer.js appends it) */
  .vw-next-card {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 0 auto 36px;
    padding: 16px 24px;
    max-width: 460px;
    width: calc(100% - 48px);
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: 14px;
    text-decoration: none;
    color: inherit;
    text-align: center;
    box-shadow: 0 2px 12px var(--shadow);
    transition: box-shadow var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-standard), border-color var(--dur-base) var(--ease-standard);
  }
.page-viewer .vw-next-card:hover {
    transform: translateY(-2px);
    border-color: var(--accent-1);
    box-shadow: 0 10px 28px var(--shadow);
  }
.page-viewer .vw-next-label {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
  }
.page-viewer .vw-next-title { font-weight: 700; font-size: 15px; color: var(--accent-1); }
@media (max-width: 640px) {
.page-viewer .vw-title { max-width: 100%; flex-basis: 100%; order: -1; }
.page-viewer .vw-toolbar { gap: 8px; padding: 8px 10px; }
.page-viewer .vw-wrap { padding: 0 8px 40px; }
/* Toolbar buttons are ~28px tall by default: fine under a mouse, too small
   under a fingertip. The full-screen toggle is one of them, and it is how a
   student gets a distraction-free read on a phone, so it has to be hittable. */
.page-viewer .vw-btn {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 44px; min-width: 44px;
    padding: 6px 12px;
  }
}

/* ── Viewer toolbar on touch ──
   The toolbar is `position: sticky; top: 0`, but `.viewer-shell` above it
   sets `overflow: hidden` — which makes the shell a scroll container, so the
   toolbar sticks to a box that never scrolls and simply rides away with the
   content. Measured on a phone: after scrolling 1500px its top was at
   -1347px, i.e. gone, taking page numbers, zoom, full screen and "Mark as
   done" with it for the rest of the document.

   Releasing the shell's overflow lets the toolbar stick to the viewport for
   real. `top` clears the fixed navbar, which measures 65px at both 390 and
   360. Touch only — desktop keeps the clipped shell exactly as it is. */
@media (hover: none) and (pointer: coarse) {
  .page-viewer .viewer-shell { overflow: visible; }
  .page-viewer .vw-toolbar {
    top: 65px;
    border-radius: 0;
  }
  /* Real fullscreen has no navbar to clear. */
  .page-viewer .viewer-shell:fullscreen .vw-toolbar { top: 0; }

  /* ── ...and then it gets out of the way ──
     Sticky solved the toolbar vanishing, but a permanent bar is its own
     problem: the controls wrap to four rows at 390px and stand 141px tall
     (51px on a desktop, which never wraps). With the 65px navbar above it,
     a quarter of the screen is chrome before a single line of the PDF.

     So it slides away. `vw-collapsed` is added by viewer.js when the reader
     SWIPES the page up, and held while in real fullscreen; swiping back down,
     or returning to the top, brings the bar back. It is driven by touchmove
     rather than by the scroll offset, because the offset moves on its own —
     see the long note in viewer.js before you change either side.

     It MUST be a transform, not display/height/padding. The bar is
     `position: sticky`, so it still occupies its space in the flow — shrink
     it and the document reflows, which moves the scroll offset, which fires
     scroll again, which flips the state straight back. The first attempt
     here did exactly that and measured as an endless 600 -> 516 -> 600
     bounce. A transform changes no layout at all, so there is nothing to
     feed back. */
  .page-viewer .vw-toolbar.vw-collapsed {
    transform: translateY(-100%);
    /* Off-screen but still in the layer tree, so it must not eat taps. */
    pointer-events: none;
  }

  /* The one control worth keeping takes its place, top-right. It is a
     SIBLING of the toolbar, not a child: anything inside a transformed
     element travels with it. `~` lets the bar's own state reveal it, so
     there is no second source of truth to keep in sync. */
  .page-viewer .vw-toolbar.vw-collapsed ~ .vw-btn.vw-fs-float {
    display: inline-flex;
    position: fixed;
    top: 73px;
    right: 12px;
    z-index: 21;
    background: var(--bg-secondary);
    box-shadow: 0 2px 10px var(--shadow);
  }
  /* Real fullscreen: no navbar above it, and this is the only chrome left. */
  .page-viewer .viewer-shell:fullscreen .vw-toolbar.vw-collapsed ~ .vw-btn.vw-fs-float {
    top: 12px;
  }
}

/* ============================================================
   LEGAL / BETA PAGES — prose container for beta-terms & privacy-notice.
   Consumes existing theme tokens only, so it themes correctly across all
   theme x mode combinations.
   ============================================================ */
.legal-doc {
  max-width: 780px;
  margin: 0 auto;
  padding: 8px 24px 90px;
  color: var(--text-secondary);
  line-height: 1.7;
}
.legal-doc .legal-meta {
  font-size: 0.9rem;
  opacity: 0.8;
  margin-bottom: 28px;
}
.legal-doc h2 {
  color: var(--text-primary);
  font-size: 1.3rem;
  margin: 34px 0 12px;
}
.legal-doc h2:first-of-type { margin-top: 6px; }
.legal-doc p { margin: 0 0 14px; }
.legal-doc ul { margin: 8px 0 16px 22px; }
.legal-doc li { margin-bottom: 7px; }
.legal-doc strong { color: var(--text-primary); }
.legal-doc a { color: var(--accent-1); }

/* Registration consent gate */
.consent-group {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 2px 0;
  text-align: left;
}
.consent-group input[type="checkbox"] {
  margin-top: 3px;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  accent-color: var(--accent-solid);
  cursor: pointer;
}
.consent-group label {
  font-size: 0.85rem;
  color: var(--text-secondary);
  line-height: 1.5;
  cursor: pointer;
}
.consent-group a { color: var(--accent-1); }

/* Beta "under construction" banner — a dismissible bottom toast. Sits low
   so it never collides with the fixed navbar's transparent-at-top math. */
.beta-banner {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: 12px;
  z-index: 1200;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 10px 16px;
  border-radius: 12px;
  background: var(--accent-solid);
  color: #fff;
  font-size: 0.9rem;
  line-height: 1.4;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28);
}
.beta-banner a { color: #fff; text-decoration: underline; }
.beta-banner .beta-banner-close {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.22);
  color: #fff;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
@media (max-width: 600px) {
  .beta-banner { left: 8px; right: 8px; bottom: 8px; font-size: 0.8rem; padding: 9px 12px; }
}

/* ============================================================
   ANNOUNCEMENTS — admin-written notices (announcements.js)

   Three looks over ONE set of tone variables:
     .ann-panel  a full card, for the homepage and landing pages
     .ann-box    a compact inline strip, for "this isn't finished yet"
     .ann-bar    a floating toast pinned to the bottom of the screen

   Every look reads --ann-solid / --ann-ink / --ann-tint / --ann-edge, and
   the .ann--<tone> class is the only thing that sets them. Adding a tone is
   therefore two custom properties in :root, one ink in the dark block and
   one line here — never a new copy of the component.

   Bars are pinned to the BOTTOM, like the beta banner, and for the same
   reason: the navbar is fixed and transparent at the top of every page, and
   anything docked up there breaks that maths. body:has(.beta-banner) lifts
   the stack so the two never sit on top of each other.
   ============================================================ */

.ann--info    { --ann-solid: var(--ann-info-solid);    --ann-ink: var(--ann-info-ink); }
.ann--warning { --ann-solid: var(--ann-warning-solid); --ann-ink: var(--ann-warning-ink); }
.ann--danger  { --ann-solid: var(--ann-danger-solid);  --ann-ink: var(--ann-danger-ink); }
.ann--success { --ann-solid: var(--ann-success-solid); --ann-ink: var(--ann-success-ink); }
.ann--neutral { --ann-solid: var(--ann-neutral-solid); --ann-ink: var(--ann-neutral-ink); }

/* Custom: --ann-solid arrives inline from the admin's validated hex. In
   light mode the colour itself is the ink; at night it is mixed towards
   white, because an admin who picks a deep colour still needs readable
   text on a dark tint of it. */
.ann--custom { --ann-ink: var(--ann-solid); }
[data-mode="dark"] .ann--custom {
  --ann-ink: color-mix(in srgb, var(--ann-solid) 52%, #ffffff);
}

.ann {
  --ann-tint: color-mix(in srgb, var(--ann-solid) 12%, transparent);
  --ann-edge: color-mix(in srgb, var(--ann-solid) 34%, transparent);
  --ann-on-solid: #ffffff;
  position: relative;
  display: flex;
  gap: 14px;
  align-items: flex-start;
  color: var(--text-primary);
}
[data-mode="dark"] .ann {
  /* A 12% wash disappears on a dark page; the tint has to carry more of
     the colour to read as a tinted surface at all. */
  --ann-tint: color-mix(in srgb, var(--ann-solid) 20%, transparent);
  --ann-edge: color-mix(in srgb, var(--ann-solid) 44%, transparent);
}

.ann-icon {
  flex-shrink: 0;
  color: var(--ann-solid);
  line-height: 0;
}
.ann-icon svg { width: 22px; height: 22px; }

.ann-main { flex: 1; min-width: 0; }
.ann-title {
  margin: 0;
  color: var(--ann-ink);
  font-weight: 700;
  line-height: 1.35;
}
.ann-body {
  margin: 4px 0 0;
  color: var(--text-secondary);
  line-height: 1.55;
  white-space: pre-line;   /* the admin's line breaks survive; nothing else does */
}
.ann-link {
  display: inline-block;
  margin-top: 8px;
  color: var(--ann-ink);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.ann-close {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--text-secondary);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  transition: var(--transition);
}
.ann-close:hover { background: var(--ann-tint); color: var(--ann-ink); }

/* ---- the in-flow stack (panel + box) ----
   Sits under the hero on every page, at the width the page's own content
   uses. `hero-only` wrappers already constrain it, so the max-width only
   bites on pages whose hero is full-bleed (the homepage). */
.ann-stack {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: min(1200px, calc(100% - 48px));
  margin: 24px auto 0;
}
.ann-stack:empty { display: none; }

/* ---- box: the workhorse ---- */
.ann-box {
  padding: 15px 17px;
  border: 1px solid var(--ann-edge);
  border-left: 4px solid var(--ann-solid);
  border-radius: 12px;
  background: var(--ann-tint);
  font-size: 0.95rem;
}
.ann-box .ann-title { font-size: 1rem; }
.ann-box .ann-body { font-size: 0.92rem; }

/* ---- panel: the homepage face ----
   Deliberately a different object from the box, not a bigger one: a card on
   the page's own surface with the tone as a rail and a wash, so it reads as
   part of the site rather than as a browser warning. */
.ann-panel {
  gap: 18px;
  padding: 26px 28px 26px 26px;
  border: 1px solid var(--card-border);
  border-radius: 18px;
  background:
    linear-gradient(var(--ann-tint), var(--ann-tint)),
    var(--card-bg);
  /* Gradient + border in one declaration — background-origin must be set
     here or the border shows a hard rim. See THEMES.md. */
  background-origin: border-box;
  box-shadow: 0 10px 30px var(--shadow);
  overflow: hidden;
}
.ann-panel::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 5px;
  background: var(--ann-solid);
}
.ann-panel .ann-icon {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: 13px;
  background: var(--ann-tint);
  border: 1px solid var(--ann-edge);
}
.ann-panel .ann-icon svg { width: 24px; height: 24px; }
.ann-panel .ann-title { font-size: 1.35rem; letter-spacing: -0.01em; }
.ann-panel .ann-body { margin-top: 6px; font-size: 1rem; }
.ann-cta {
  display: inline-block;
  margin-top: 14px;
  padding: 10px 20px;
  border-radius: 10px;
  background: var(--ann-solid);
  color: var(--ann-on-solid);
  font-weight: 600;
  font-size: 0.95rem;
  text-decoration: none;
  transition: var(--transition);
}
.ann-cta:hover { filter: brightness(1.08); transform: translateY(-1px); }

/* ---- bar: the floating toast ---- */
.ann-bar-stack {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: 12px;
  z-index: 1190;          /* under the beta banner's 1200, which sits below */
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
}
body:has(.beta-banner) .ann-bar-stack { bottom: 74px; }
.ann-bar {
  pointer-events: auto;
  align-items: center;
  gap: 12px;
  max-width: 760px;
  margin: 0 auto;
  width: 100%;
  padding: 12px 16px;
  border: 1px solid var(--ann-edge);
  border-radius: 12px;
  background: var(--ann-solid);
  color: var(--ann-on-solid);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28);
  font-size: 0.9rem;
}
.ann-bar .ann-icon,
.ann-bar .ann-title,
.ann-bar .ann-body,
.ann-bar .ann-link { color: var(--ann-on-solid); }
.ann-bar .ann-title { font-size: 0.92rem; }
.ann-bar .ann-body { margin-top: 2px; font-size: 0.88rem; opacity: 0.92; }
.ann-bar .ann-link { margin-top: 0; margin-left: 6px; }
.ann-bar .ann-close {
  background: rgba(255, 255, 255, 0.22);
  color: var(--ann-on-solid);
  width: 24px;
  height: 24px;
}
.ann-bar .ann-close:hover { background: rgba(255, 255, 255, 0.34); }

@media (max-width: 600px) {
  .ann-stack { width: calc(100% - 32px); margin-top: 18px; }
  .ann-panel { padding: 20px; gap: 14px; }
  .ann-panel .ann-title { font-size: 1.15rem; }
  .ann-panel .ann-icon { width: 40px; height: 40px; }
  .ann-bar-stack { left: 8px; right: 8px; bottom: 8px; }
}

/* Legal links injected into every footer by script.js */
.footer-legal { text-align: center; margin-top: 8px; }
.footer-legal a {
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin: 0 8px;
  text-decoration: none;
}
.footer-legal a:hover { color: var(--text-primary); }


/* ============================================================
   TOUCH DEVICES ONLY
   Gated on `(hover: none) and (pointer: coarse)` — "the main pointer is a
   finger" — NOT on width. A desktop window dragged narrow keeps a mouse, so
   it never matches, and the desktop experience is untouched by design.
   A touchscreen laptop reports `pointer: fine` for its trackpad, so it is
   excluded too. Sizes below come from measuring the real pages at 390px.
   ============================================================ */
@media (hover: none) and (pointer: coarse) {

  /* ── The navbar is the worst backdrop-filter on the page ──
     It is fixed, and at <=768px it is always frosted, so the ENTIRE page
     scrolls behind a live blur — recomputed every frame of every scroll,
     the biggest single scroll cost on a phone. A solid bar is the standard
     mobile app-bar look and costs nothing. Gated on pointer type, not the
     768px width rule it overrides, so a narrowed desktop window keeps its
     frosted bar unchanged. */
  .navbar,
  .navbar.top-hint,
  .navbar.scrolled {
    background: var(--bg-primary);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  /* Hero animations pause once their host scrolls out of view (script.js
     adds .anim-paused). Off-screen, so invisible; it only stops the
     continuous compositor work behind every backdrop-filter. */
  .anim-paused .hero-blob,
  .anim-paused .hero-symbol,
  .anim-paused .hero-motif,
  .anim-paused.hero-blob,
  .anim-paused .floating {
    animation-play-state: paused;
  }

  /* ── The open menu drawer needs a floor ──
     `--nav-bg` is 70-78% opaque in every theme. As a thin bar over the top of
     a page that reads as frosted glass; as a full panel dropped over a
     saturated hero it just makes the links unreadable, with the hero's
     headline showing straight through the words. `--bg-primary` is the one
     surface token that is fully opaque in all six theme/mode combinations,
     so the panel gets a real floor without inventing a colour (see
     THEMES.md). The blur goes with it — there is nothing left to blur, and
     it was costing a full-panel backdrop-filter on the weakest GPUs.
     Touch only, and in any case desktop has no drawer: `.hamburger` is
     `display: none` until 768px, where `.nav-links` stops being the
     horizontal navbar and becomes this panel. */
  .nav-links.open {
    background: var(--bg-primary);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  /* ── Tap targets ──
     Apple and Google both ask for ~44px. These were measured too small:
     menu rows 342x21, footer links 59x19, the legal row 146x18, and the
     "Remember me" label 107x17. Width was fine everywhere — only the
     vertical size was short, so these add height without moving anything
     sideways. (The register consent checkbox is NOT here: its label
     measures 224x126, so the real target is already large.) */
  .nav-links.open .nav-link {
    min-height: 44px;
    display: flex;
    align-items: center;
  }
  .footer-links a,
  .footer-social a,
  .footer-legal a,
  .footer-bottom a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .checkbox-row {
    min-height: 44px;
  }
  .checkbox-row input {
    width: 20px;
    height: 20px;
  }

  /* ── Sticky hover ──
     A tap leaves :hover applied until you tap somewhere else, so a card or
     button stays visibly lifted — it reads as a stuck UI. Only the movement
     is cancelled; the colour and border hovers stay, because on touch they
     double as useful "you hit it" feedback. */
  .btn-primary:hover,
  .btn-secondary:hover,
  .btn-register:hover,
  .icon-btn:hover,
  .scroll-top:hover,
  .theme-option:hover,
  .xm-btn:hover,
  .note-card:hover,
  .testimonial-card:hover,
  .path-card:hover,
  .class-card:hover,
  .exam-card:hover,
  .access-cta--online:hover,
  .access-cta--offline:hover,
  .access-cta--locked:hover,
  .page-404 .nf-btn:hover,
  .page-profile .avatar-option:hover,
  .feature-card:hover .feature-icon,
  .subject-card:hover .subject-icon,
  .subject-page .lib-sec-card:hover .lib-btn-arrow,
  .subject-page .lib-topic-card:hover .lib-btn-arrow,
  .nsub-card:hover .lib-btn-arrow,
  .cont-chip:hover .lib-btn-arrow {
    transform: none;
  }

  /* ── Viewport height ──
     `100vh` on a phone means the viewport with the address bar HIDDEN, so a
     full-height section is taller than what you can see and the page gains a
     phantom scroll. `dvh` tracks the bar sliding in and out. The plain vh
     line stays first as the fallback for older Android browsers. */
  /* A full-screen hero centres its content, which on a phone leaves ~335px
     of empty gradient above the headline — 40% of the screen saying nothing.
     (The headline and buttons were always visible; the space was the waste,
     not hidden content.) Trimming to 82dvh keeps the hero dominant but lets
     the next section show at the bottom edge, so the page reads as
     scrollable instead of ending there. */
  .hero {
    min-height: 100vh;
    min-height: 82dvh;
  }
  .login-page {
    min-height: 100vh;
    min-height: 100dvh;
    /* The card was centred in a full-height section, so on a tall phone it
       floated near the middle with a big empty band above it — the beta
       welcome box read as sitting too low. Anchoring to the top puts it
       where the eye lands first and where a thumb reaches. 80px clears the
       65px touch navbar with air to spare (desktop keeps its 104px, sized
       for the taller ~88px bar). Shared by welcome, login, register,
       reset-password and forgot-password. */
    align-items: flex-start;
    padding-top: 80px;
  }

  /* ── Tap targets, second pass ──
     The first pass took the footer and the open menu. These are the rest,
     measured at 390px. Two techniques, chosen per element:

     (a) REAL SIZE — min-height/padding, for anything in normal flow with room
         to grow.
     (b) INVISIBLE HIT AREA — a `::after` stretched past the element's box.
         Used where growing the element would move the layout. The navbar is
         the reason this exists: nothing in that row can flex-shrink (see the
         NAVBAR FIT tiers above), so making its buttons 44px would push the
         hamburger off-screen under ~540px. This grows only what a finger
         hits, so the row measures exactly as it does today. */

  /* The back link — 18px tall, the smallest target on the site, and on a
     phone it IS the way back: there is no browser back button in view the
     way there is on desktop. It sits on its own line, so it can take real
     padding without disturbing anything. */
  .lib-sec-back {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 4px 2px;
  }
  /* On the section-card grid it spans the whole row (grid-column: 1 / -1),
     which made a 390px-wide strip that only responded in an 18px band.
     Centre the text left and let the whole strip work. */
  .subject-page .lib-sec-grid > .lib-sec-back { justify-content: flex-start; }

  /* Navbar: hit area only, no layout change (technique b). The logo is here
     too — it is a link home on every page, and its image is 40px tall. */
  .icon-btn,
  .btn-login,
  .logo {
    position: relative;
  }
  .icon-btn::after,
  .btn-login::after,
  .logo::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: max(100%, 44px);
    height: max(100%, 44px);
    transform: translate(-50%, -50%);
  }

  /* Standalone links: REAL height, never an overlay.
     An overlay does not change layout, so two links stacked close together
     both grow invisibly into each other and the lower one — later in the DOM
     — steals the tap. That is exactly what happened on welcome.html, where
     "Log in" and the support address sit 6px apart and tapping Log in opened
     the mail app. Real height grows the line box, so the lines push apart and
     an overlap cannot happen by construction. Overlays are now used ONLY in
     the navbar, where the row is fixed-height and the items sit side by side
     with no vertical neighbour. */
  .subject-breadcrumb-list a,
  .forgot-link,
  .footer-contact-link,
  .login-switch a,
  .lib-empty a,
  .beta-banner a,
  .ann-link,
  .detail-text a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* ── iOS: stop Safari zooming into every field ──
     Safari on iOS zooms the whole page when a focused field's text is under
     16px, and it does not zoom back out — the reader is left scrolled
     sideways on a page that no longer fits. Every form field here was 14px
     (15px for the notes search). 16px is the threshold, not a preference.
     Harmless on Android, which never had the behaviour, and slightly easier
     to read there. Checkboxes and radios are excluded: no text, and sizing
     them is handled above. The content manager is excluded too — it is an
     admin desktop tool with dense tables that this would reflow. */
  :not(.page-cm) input:not([type='checkbox']):not([type='radio']),
  :not(.page-cm) select,
  :not(.page-cm) textarea {
    font-size: 16px;
  }

  /* ── iOS: the notch and the home bar ──
     `env(safe-area-inset-*)` is 0 on every device without a cutout, so the
     max() keeps the existing values on Android and only grows where a real
     inset exists. Without this the fixed navbar slides under the notch in
     landscape, and bottom-anchored UI sits under the home indicator. */
  .nav-container {
    padding-left: max(16px, env(safe-area-inset-left));
    padding-right: max(16px, env(safe-area-inset-right));
  }
  .xm-topbar {
    padding-left: max(24px, env(safe-area-inset-left));
    padding-right: max(24px, env(safe-area-inset-right));
  }
  .beta-banner {
    bottom: max(8px, env(safe-area-inset-bottom));
  }
  /* The banner rides along on welcome.html and beta-terms.html — the first
     screens every invited student sees. Its dismiss button was 24px; its
     "Learn more" link is handled with the standalone links above. */
  .beta-banner .beta-banner-close {
    min-width: 44px;
    min-height: 44px;
  }

  /* Announcements: the dismiss button is the only thing a finger aims at in
     a notice, and it sits beside text — it gets real size, not an overlay.
     The panel's CTA is a button already, so it only needs the floor. */
  .ann-close {
    min-width: 44px;
    min-height: 44px;
  }
  .ann-cta {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* Real size — these all sit in flow with room around them. */
  .toggle-password,
  .subject-hero .subject-share-btn,
  .lib-retry-btn,
  .note-download {
    min-height: 44px;
    min-width: 44px;
  }
  .subject-hero .subject-share-btn,
  .lib-retry-btn,
  .note-download {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* ── No grey box on tap ──
     Android paints `-webkit-tap-highlight-color` over the WHOLE hit area of
     any tapped link or button — including the 44px areas grown above and the
     invisible ::after overlays — so enlarging the targets is exactly what
     made the flash big and obvious. The target stays finger-sized; only the
     paint goes, so just the text reads as clickable.

     This is NOT the `:focus-visible` ring at the top of the file: tapping a
     button on Android fires :focus but never :focus-visible, so that rule
     never draws here. It stays untouched, and keyboard focus keeps its ring
     on every device. */
  a,
  button,
  label,
  summary,
  input,
  select,
  textarea,
  [role='button'],
  [tabindex] {
    -webkit-tap-highlight-color: transparent;
  }

  /* ── The password eye, made identical on all four pages ──
     login, register, reset and profile each grew their own toggle while the
     CSS still lived in their HTML, so the eye sat at a different size and a
     different distance from the field edge on each. They now share one SVG
     (password-toggle.js) but not one position.

     Unified HERE ONLY, not in the page rules above: the SVG is sized in `em`,
     so touching those font-sizes would resize the icon on desktop too, and
     desktop is frozen until beta. Fold these back into a single base rule
     after launch.

     `right: 0` puts the 44px target flush against the field's right end —
     the icon centres inside it, a fixed 22px in from the edge on every page.
     Each selector is scoped to match the page rules' (0,2,0) specificity;
     a bare `.toggle-password` would lose to them no matter where it sits. */
  .login-page .toggle-password,
  .page-register .toggle-password,
  .page-reset .toggle-password,
  .page-profile .toggle-password {
    right: 0;
    padding: 0;
    font-size: 15px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* Room for it. Register is the one that actually needed this — its input
     never had a right padding (see the note by .page-register .toggle-password),
     so a flush eye would sit on top of the typed password. The others are
     re-stated to land on one number. */
  .login-page .password-input-wrap input,
  .page-register .password-wrapper input,
  .page-profile .password-input-wrap input {
    padding-right: 48px;
  }
  /* Register's wrapper is a flex row and its input never claimed a width, so
     the field only ever grew to its intrinsic ~177px while the wrapper spanned
     the card. The eye is positioned against the WRAPPER, so anchoring it right
     put it in open space past the field's end. Filling the wrapper makes the
     two edges the same edge. (Desktop has this gap too — it is frozen, so the
     fix is scoped to touch and the desktop case is logged in the handover.) */
  .page-register .password-wrapper input {
    width: 100%;
  }

  /* ── Breadcrumbs ──
     Hidden outright on a phone (owner's call). The trail costs a line or
     two of a small screen to say what the page already says, the phone's
     own back button covers going back one step, and every deep page carries
     an in-page way up a level.

     That last part was checked page by page rather than assumed. The hubs
     (notes/exams/classes/recordings/learn) only ever show "Home > X", and
     Home is one tap away in the menu. The deep pages each render their own
     link: "All subjects" on a notes or exam subject, "Classes" on a
     recording card, "All classes" on the recordings hub.

     The ONE page that would be stranded is the viewer, where the trail is
     the only route back to the subject — and it is untouched here, because
     it uses `.vw-breadcrumb` outside any hero rather than this class. Keep
     it that way: hiding it needs a back link added first.

     This replaces an earlier fix that put the trail back in flow so a
     wrapped one pushed the hero title down instead of landing on top of it
     (the recording watch page reaches five levels). Hiding it outright
     settles that case too. */
  .subject-hero .subject-breadcrumb {
    display: none;
  }

  /* The Live / Recorded switch is the main navigation on the classes hub
     now, at 35px tall. */
  .classes-switch-btn {
    min-height: 44px;
  }
}

/* ============================================================
   TOUCH DEVICES ONLY — NO LIVE BLUR, ANYWHERE

   A `backdrop-filter` is recomputed every frame the page moves behind it.
   The navbar and the drawer were done first because they are the worst
   offenders; this finishes the job for the remaining ~30 blurred surfaces
   (cards, hero badges, stat cards, toolbars, the sudo scrim). Testing on a
   real Android confirmed scrolling was not smooth, which is what this is
   for — it is not a theoretical cost.

   Two halves, and BOTH are needed:

   1. The blur goes. A blanket `*` rule rather than a list of 30 selectors,
      so a card added later is covered on a phone by default instead of
      quietly reintroducing the problem.
   2. `--card-bg` turns opaque. Dropping the blur ALONE would be worse than
      leaving it: every card is 55-74% translucent, and without the blur the
      page and the animated hero blobs show through razor-sharp. So the token
      becomes the flat colour the blurred card already resolved to.

   Each value is its theme's `--card-bg` composited over that theme's
   `--bg-primary` — the ground a card actually sits on — so the surface lands
   on the colour it already appeared to be. A card sitting on a
   `--bg-secondary` section shifts by a few values per channel; that is below
   the noticeable threshold and is the price of one token instead of 87 rules.

   The six selectors repeat the palette ladder from the top of this file IN
   THE SAME ORDER, which is what makes the cascade resolve identically:
   the two `[data-theme][data-mode]` blocks are (0,2,0) and beat the rest,
   and among the (0,1,0) rules the later one wins exactly as it does above.
   Adding a brand means adding its line here too — see THEMES.md.
   ============================================================ */
@media (hover: none) and (pointer: coarse) {
  :root                                    { --card-bg: #fdfefd; }
  [data-mode="dark"]                       { --card-bg: #0a130d; }
  [data-theme="indigo"]                    { --card-bg: #fbfcfe; }
  [data-theme="indigo"][data-mode="dark"]  { --card-bg: #1d1d28; }
  [data-theme="azure"]                     { --card-bg: #fbfcfe; }
  [data-theme="azure"][data-mode="dark"]   { --card-bg: #1c202d; }
  [data-theme="autumn"]                    { --card-bg: #fef9f1; }
  [data-theme="autumn"][data-mode="dark"]  { --card-bg: #1c1109; }
  [data-theme="meadow-lifted"]                   { --card-bg: #fdfefd; }
  [data-theme="meadow-lifted"][data-mode="dark"] { --card-bg: #0a130d; }

  /* `!important` because these override rules that are more specific than any
     bare property selector could be, and the intent is absolute: on a phone
     there is no live blur, full stop. The sudo modal loses its blur too,
     which simply makes it match the exam and command-palette modals — both
     already ship a plain scrim at the same opacity and no blur. */
  *,
  *::before,
  *::after {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
}

/* ===== RECORDING PLAYER (recording.html) ===== */
/* The hero is slimmer than a subject hero — the video is the point of the
   page, so it must start above the fold on a laptop. */
.rec-hero { min-height: 0; padding-bottom: 28px; }
.rec-player-wrap { max-width: 960px; margin: 26px auto 0; padding: 0 24px 24px; }
.rec-player {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  border: 1px solid var(--card-border);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 6px 20px var(--shadow);
}
.rec-player iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* The locked face keeps the same 16:9 box, so the page doesn't jump when
   access changes — only what fills the frame does. */
.rec-player--locked { background: var(--card-bg); }
.rec-lock-panel {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 24px;
  text-align: center;
}
.rec-lock-panel .lib-icon-lock { width: 34px; height: 34px; stroke: var(--text-secondary); }
.rec-lock-panel p { color: var(--text-secondary); font-size: 15.5px; max-width: 420px; }
.rec-desc { max-width: 960px; margin: 18px auto 0; padding: 0 24px; text-align: left; }
.rec-nav { display: flex; gap: 18px; flex-wrap: wrap; max-width: 960px; margin: 14px auto 0; padding: 0 24px; }
.rec-nav .rec-nav-next { margin-left: auto; }

/* Recordings on touch devices. This sits here, immediately after the rules
   it overrides, rather than in the main TOUCH DEVICES ONLY block above —
   that block comes EARLIER in the file, so its overrides lost to these on
   source order. Keep any further recordings-on-touch rules here with them. */
@media (hover: none) and (pointer: coarse) {
  /* The video goes edge to edge. On a 390px screen the 24px side padding
     costs about 14% of the picture, and on a phone the picture IS the
     experience. `.subject-page-wrap` adds no side padding of its own, so
     dropping this one is enough for a true full-bleed frame. The rounded
     corners and side borders go too: a 16px radius hard against the screen
     edge reads as a mistake. */
  .rec-player-wrap {
    padding-left: 0;
    padding-right: 0;
  }
  .rec-player {
    border-radius: 0;
    border-left: 0;
    border-right: 0;
  }
  /* Prev/next video links sit in a row of their own. */
  .rec-nav a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* The correction popup's ✕. Its Cancel/Send buttons are already 44px;
     this one is a 34px circle on desktop, which a finger misses. */
  .corr-close { width: 44px; height: 44px; top: 10px; right: 10px; }
  .corr-title { margin-right: 56px; }   /* clears the bigger ✕ */
}

/* ----- One note's history, in the content manager -----
   Two lists in one modal: what changed, and what it can be put back to.
   Same row shape for both so they read as one panel. */
.page-cm .cm-subhead {
    margin: 18px 0 8px;
    font-size: 15px;
    font-weight: 700;
    color: var(--text-primary);
}
.page-cm .cm-hist-list { display: flex; flex-direction: column; gap: 8px; }
.page-cm .cm-hist-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    border: 1px solid var(--card-border);
    border-left: 3px solid var(--card-border);
    border-radius: 10px;
    background: var(--bg-secondary);
    padding: 10px 14px;
}
/* A publish that removed most of the note. Marked, not hidden. */
.page-cm .cm-hist-row.flagged { border-left-color: var(--accent-1); }
.page-cm .cm-hist-what { color: var(--text-primary); font-size: 14.5px; }
.page-cm .cm-hist-who { color: var(--text-secondary); font-size: 13px; margin-top: 3px; }

@media (hover: none) and (pointer: coarse) {
    .page-cm .cm-hist-row .cm-btn { min-height: 44px; }
}

/* ============================================================
   THE EDITING DESK (editing-desk.html)

   An Editor's page. It wears .page-cm as well as .page-ed, so every
   card, tab, table, button, modal and maths preview above already
   applies — this block is only the handful of things the content
   manager has no equivalent of. Deliberately small: if it needed a
   design of its own, the two pages would be doing different jobs,
   and they are not.

   Every colour here is a token. See THEMES.md — nothing in this
   block may hardcode a hex, and every accent FILL takes its ink from
   --text-on-accent, never #fff: azure and indigo dark use dark ink on
   theirs, and a hardcoded white would vanish.
   ============================================================ */
.page-ed .ed-sub { font-size: 14px; color: var(--text-secondary); }

/* The count of corrections waiting, on the tab itself. It is the only
   thing on the page that says "there is work", so it sits where the
   eye already is rather than inside the panel it describes. */
.page-ed .ed-pill {
    display: inline-block;
    min-width: 20px;
    padding: 1px 7px;
    margin-left: 6px;
    border-radius: 50px;
    background: var(--accent-solid);
    color: var(--text-on-accent);
    font-size: 12px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    vertical-align: 1px;
}
/* `display: inline-block` above beats the UA's `[hidden] { display: none }`,
   so without this the pill sits there reading "0" forever. */
.page-ed .ed-pill[hidden] { display: none; }

.page-ed .ed-muted { color: var(--text-secondary); font-size: 13.5px; }

/* ----- one test's questions ----- */
.page-ed .ed-q-list { display: flex; flex-direction: column; gap: 10px; }
.page-ed .ed-q-row {
    border: 1px solid var(--card-border);
    border-radius: 10px;
    background: var(--bg-secondary);
    padding: 12px 14px;
}
.page-ed .ed-q-head { display: flex; align-items: flex-start; gap: 12px; }
.page-ed .ed-q-num {
    flex: 0 0 auto;
    width: 26px; height: 26px;
    border-radius: 50%;
    background: var(--accent-solid);
    color: var(--text-on-accent);
    font-size: 13px; font-weight: 700;
    display: flex; align-items: center; justify-content: center;
}
.page-ed .ed-q-text {
    flex: 1;
    color: var(--text-primary);
    font-size: 14.5px;
    line-height: 1.55;
    overflow-x: auto;    /* a long display equation scrolls, never widens the modal */
    white-space: pre-line;   /* the question's own line breaks — see .xm-q-text */
}
.page-ed .ed-q-head .cm-btn { flex: 0 0 auto; }

/* The tick beside the correct option. Shown, never movable: a typo in
   the right answer is exactly what gets reported, so an editor has to
   be able to read it — but which one is right is not theirs to change. */
.page-ed .ed-opt-mark {
    flex: 0 0 auto;
    width: 18px;
    color: var(--accent-1);
    font-weight: 700;
    text-align: center;
}

/* ----- corrections ----- */
.page-ed .ed-corr-list { display: flex; flex-direction: column; gap: 12px; }
.page-ed .ed-corr {
    border: 1px solid var(--card-border);
    border-radius: 12px;
    background: var(--bg-secondary);
    padding: 14px 16px;
}
.page-ed .ed-corr-head {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: 12px; margin-bottom: 8px;
}
.page-ed .ed-corr-head strong { color: var(--text-primary); font-size: 15px; }
.page-ed .ed-corr-msg {
    color: var(--text-primary);
    font-size: 14.5px;
    line-height: 1.6;
    white-space: pre-wrap;      /* a student's line breaks are part of what they said */
    overflow-x: auto;
}
.page-ed .ed-corr-reply {
    margin-top: 10px;
    padding: 8px 12px;
    border-left: 3px solid var(--accent-1);
    background: var(--card-bg);
    border-radius: 0 8px 8px 0;
    color: var(--text-secondary);
    font-size: 14px;
}
.page-ed .ed-corr-actions { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }

/* Phones: tap targets, and rows that stack rather than squeeze.
   Gated on touch, never on width — see CLAUDE.md. */
@media (hover: none) and (pointer: coarse) {
    .page-ed .ed-corr-actions .cm-btn,
    .page-ed .ed-q-head .cm-btn { min-height: 44px; }
    .page-ed .ed-q-head { flex-wrap: wrap; }
    .page-ed .ed-q-text { flex-basis: 100%; order: 3; }
}

/* ============================================================
   REDUCED MOTION — global, deliberately last so it wins every cascade.

   For visitors whose OS asks for less motion (vestibular disorders,
   migraine triggers): every animation collapses to its final frame and
   smooth scrolling turns off. Near-zero duration rather than
   `animation: none` so entry animations (cardIn, accessPopIn) still END
   VISIBLE at their final state instead of never running — `none` would
   leave anything animated-in stuck invisible. Continuous loops (hero
   blobs/motifs, pulseDot, skelShimmer) freeze on their resting frame.
   Nothing here changes what renders for anyone else.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    /* delay must go too: the hero motifs stagger 0-2.5s, and a delayed
       one-shot still fires visibly seconds after load */
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
