/**
 * shared.css — Design system and sidebar styles for French Study Suite
 * Import in every page: <link rel="stylesheet" href="shared.css">
 */

/* ─── Google Fonts (loaded via HTML link in each page) ─────────────────────── */
/* Playfair Display 700/800 + DM Sans 400/500/600 */

/* ─── CSS Variables ─────────────────────────────────────────────────────────── */
:root {
    /* Dark-theme palette */
    --bg: #0d1117;
    --surface: #161b22;
    --surface-2: #21262d;
    --border: #30363d;

    --accent: #f0a500;
    --accent-soft: #f0a50022;
    --teal: #00b4d8;
    --teal-soft: #00b4d822;
    --red: #ff6b6b;
    --red-soft: #ff6b6b22;
    --green: #3dd68c;
    --green-soft: #3dd68c22;
    --purple: #b197fc;
    --purple-soft: #b197fc22;
    /* M219 — the one new token drills' rewrite needs. --red/--green
       already cover danger/success semantics; nothing existing covered
       "warning" (amber-ish, distinct from --accent, which varies too much
       per theme — blue in [data-theme="app"] — to double as "warning"). */
    --warning: #ffb74d;
    --warning-soft: #ffb74d22;

    --text: #e6edf3;
    --text-muted: #8b949e;
    --text-dim: #484f58;

    --font-display: "Playfair Display", serif;
    --font-body: "DM Sans", sans-serif;

    --radius: 12px;
    --radius-lg: 20px;
    --transition: 0.2s ease;

    /* Elevation scale (M159) — before this, every box-shadow in the app was
       an independently hardcoded literal (confirmed: zero --shadow* tokens
       existed anywhere). These 3 tiers are DESCRIPTIVE, not invented — each
       matches a real, already-shipped value exactly: --shadow-s = #nav-bar's
       shadow, --shadow-m = .ub-popover's (nav profile popover), --shadow-l =
       login/register .card's. Swapping those 3 specific rules onto the new
       tokens is therefore pixel-identical, not a redesign. Deliberately NOT
       touching [data-theme="app"]'s own elevation mechanism further down
       this file ([class*="-card"] box-shadow etc.) — that's a separate,
       already-good, deliberate system for reskinning any "-card" class,
       not something this generic scale should absorb or replace. */
    --shadow-s: 0 2px 10px rgba(0, 0, 0, 0.25);
    --shadow-m: 0 8px 28px rgba(0, 0, 0, 0.35);
    --shadow-l: 0 24px 60px rgba(0, 0, 0, 0.5);

    /* Spacing scale (M151) — a 4px base unit, matching the padding/gap
       values already in common use across the app's per-page CSS before
       this existed (1.5rem page-header padding, .75rem/1rem alert
       padding, etc). Additive only: nothing was migrated onto these yet,
       existing rules keep their own literal values until a later
       milestone touches them. */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-7: 32px;
    --space-8: 40px;

    /* Full-page menu content max-width — NOT a rail width; #sidebar itself is
       always 100vw. Genuinely uncapped (the viewport IS the limit) by
       default; an admin can optionally cap it via "Menu Content Width"
       (600-1400px) in the Design tab, which nav.js only overrides this
       variable for when a real value has been explicitly saved. */
    --sidebar-w: none;
}

/* ─── Theme: light ─────────────────────────────────────────────────────────── */
[data-theme="light"] {
    --bg: #f4f6f9;
    --surface: #ffffff;
    --surface-2: #e9ecf0;
    --border: #d0d7de;
    --accent: #c97800;
    --accent-soft: #f0a50022;
    --teal: #0070a8;
    --teal-soft: #0070a822;
    --red: #d73a49;
    --red-soft: #d73a4918;
    --green: #1a7f37;
    --green-soft: #1a7f3718;
    --purple: #6f42c1;
    --purple-soft: #6f42c118;
    --warning: #b45309;
    --warning-soft: #f59e0b18;
    --text: #1c2128;
    --text-muted: #57606a;
    --text-dim: #8c959f;

    /* Navy-tinted, not black — matches the light-context shadow tint already
       proven in this app's own cream-Tailwind pages (css/path.css/
       css/drill.css both already use rgba(10,22,40,*) for exactly this
       reason: a pure-black shadow reads muddy on a light surface). */
    --shadow-s: 0 4px 12px rgba(10, 22, 40, 0.1);
    --shadow-m: 0 8px 28px rgba(10, 22, 40, 0.18);
    --shadow-l: 0 24px 60px rgba(10, 22, 40, 0.32);
}

/* ─── Theme: ocean ─────────────────────────────────────────────────────────── */
[data-theme="ocean"] {
    --bg: #050d1a;
    --surface: #0a1628;
    --surface-2: #122040;
    --border: #1e3a5f;
    --accent: #f0a500;
    --accent-soft: #f0a50022;
    --teal: #00e5ff;
    --teal-soft: #00e5ff18;
    --red: #ff5f7e;
    --red-soft: #ff5f7e18;
    --green: #1de9b6;
    --green-soft: #1de9b618;
    --purple: #82b1ff;
    --purple-soft: #82b1ff18;
    --warning: #ffca80;
    --warning-soft: #ffca8018;
    --text: #daf0ff;
    --text-muted: #7eb3d8;
    --text-dim: #3a6b92;
}

/* ─── Theme: forest ────────────────────────────────────────────────────────── */
[data-theme="forest"] {
    --bg: #060e07;
    --surface: #0c1a0e;
    --surface-2: #152318;
    --border: #253d28;
    --accent: #f0a500;
    --accent-soft: #f0a50022;
    --teal: #69f0ae;
    --teal-soft: #69f0ae18;
    --red: #ff5252;
    --red-soft: #ff525218;
    --green: #b9f6ca;
    --green-soft: #b9f6ca18;
    --purple: #ce93d8;
    --purple-soft: #ce93d818;
    --warning: #ffd180;
    --warning-soft: #ffd18018;
    --text: #e0f2e0;
    --text-muted: #81c784;
    --text-dim: #3a6b3e;
}

/* ─── Theme: app ───────────────────────────────────────────────────────────────
   Native-app shape language (Material/iOS-ish), not a color swap — bigger
   corner radii, elevation shadows instead of hard borders, pill buttons, and
   a single sans font for both display and body (no serif "editorial" feel).
   Colors lean light/clean since that's the native-app default on both
   platforms; dark/light/ocean/forest already cover the moodier palettes. */
[data-theme="app"] {
    --bg: #f2f4f8;
    --surface: #ffffff;
    --surface-2: #eef1f6;
    --border: #e3e7ee;

    --accent: #2f6fed;
    --accent-soft: #2f6fed1a;
    --teal: #12b5a6;
    --teal-soft: #12b5a61a;
    --red: #ff3b30;
    --red-soft: #ff3b301a;
    --green: #30c85e;
    --green-soft: #30c85e1a;
    --purple: #7c5cff;
    --purple-soft: #7c5cff1a;
    --warning: #f59e0b;
    --warning-soft: #f59e0b1a;

    --text: #14181f;
    --text-muted: #5b6472;
    --text-dim: #9aa3b2;

    /* Same family for both — every page already renders h1/h2/h3 in
       --font-display, so this alone drops the serif "magazine" look
       sitewide without touching a single page's markup. */
    --font-display: var(--font-body);

    --radius: 16px;
    --radius-lg: 26px;
}
/* Elevation instead of hard borders — `border-color: transparent` beats a
   page's own `border:1px solid var(--border)` (same property; this
   attribute-selector's specificity outranks a plain single-class rule),
   because only the border's COLOR needs to disappear; the box-shadow
   supplies the visual separation Material/iOS use instead. Matches any
   "*-card" class already used throughout the app (`.s-card`,
   `.dictee-card`, `.task-card`, `.concept-card`, ...) with zero per-page
   changes required. Deliberately NOT matching "-box"/"-panel" too — several
   of those nest INSIDE a "-card" (e.g. `.situation-box` inside a card), and
   shadowing both would double up visual weight. */
[data-theme="app"] [class*="-card"] {
    border-color: transparent !important;
    box-shadow: 0 2px 16px rgba(20, 24, 31, 0.07), 0 1px 3px rgba(20, 24, 31, 0.05);
}
/* Pill buttons — Material/iOS both default to fully-rounded CTAs. Circular
   icon buttons (.tts-btn, .voice-btn, .theme-swatch) are unaffected since
   999px on an equal-width/height element still renders as a circle. */
[data-theme="app"] [class*="-btn"] {
    border-radius: 999px !important;
}
[data-theme="app"] #sidebar {
    box-shadow: 2px 0 24px rgba(20, 24, 31, 0.06);
}
[data-theme="app"] #sidebar-toggle {
    box-shadow: 0 2px 10px rgba(20, 24, 31, 0.18);
}
[data-theme="app"] .s-tag {
    border-radius: 999px;
}

/* ─── Page header (M152) — canonical rule, consolidated from 25 duplicated
   per-page copies (one in each tool's own css/*.css). Previously every
   page hardcoded a fixed-dark gradient (#161b22 → #0d1117) regardless of
   theme — harmless under dark/ocean/forest since --text was already light
   there, but a real, pre-existing bug under Light theme (and later App
   theme), which flip --text dark: the h1 title (every page leaves it
   uncolored, inheriting --text) went invisible against that fixed-dark
   background, patched only by forcing h1 white via body.dark-page's extra
   specificity — which then silently defeated the one page (tool-a) that
   had already, correctly, set color:var(--text) on it itself. Using
   --surface/--bg here instead of their hardcoded dark-theme values fixes
   this for real: the gradient now derives from whichever theme is active,
   pixel-identical to before under the default dark theme (dark theme's
   --surface/--bg equal the old hardcoded hex), and automatically legible
   under every other theme too — --text already resolves to a contrasting
   color per theme, so the old forced-white h1 patch is gone; h1 simply
   inherits --text like every page already assumed it did. */
.page-header {
    background: linear-gradient(135deg, var(--surface), var(--bg));
    border-bottom: 1px solid var(--border);
    padding: var(--space-6) var(--space-6) var(--space-4);
}
/* App Theme: every page's header becomes an on-brand blue gradient instead
   of the theme-derived surface/bg above — reinforces the "native app"
   identity sitewide, not just on settings.html. This is deliberate
   per-theme branding, not the bug above — it stays exactly as it was,
   just now overriding a token-driven base instead of a hardcoded one. */
[data-theme="app"] .page-header {
    background: linear-gradient(135deg, var(--accent), #1f52c4) !important;
}
/* App Theme's own blue override (immediately above) is a fixed brand
   color, not derived from --surface/--bg, so it still needs its own
   subtitle-contrast fix independent of the base rule's theme-awareness:
   --text-muted reads fine against every OTHER theme's header but is
   low-contrast against this one deliberately bright blue. */
[data-theme="app"] .page-header p {
    color: rgba(255, 255, 255, 0.75);
}

/* ─── Filter pills / chips (M153) — canonical rule, consolidated from 7
   duplicated per-page copies (majority recipe: 5 files matched exactly).
   tool-e-shadowing-practice.css/tool-f-roleplay.css keep a small residual
   override for their real, intentional smaller/faster variant — see their
   own files. Each page's own `.pill.active` color (purple/teal/accent)
   stays page-local; that's legitimate per-page theming, not fragmentation. */
.pills {
    display: flex;
    gap: .4rem;
    flex-wrap: wrap;
}
.pill {
    padding: .35rem .8rem;
    border-radius: 99px;
    font-size: .8rem;
    font-weight: 600;
    cursor: pointer;
    border: 2px solid var(--border);
    background: transparent;
    color: var(--text-muted);
    transition: all .15s;
}

/* ─── Global mobile touch polish (every page, dark or Tailwind) ────────────── */
html, body {
    -webkit-text-size-adjust: 100%;
    -webkit-tap-highlight-color: transparent;
    /* #sidebar is `position:fixed` + `transform:translateX(-100%)` when
       collapsed off-canvas (below). A CSS transform on a fixed element makes
       browsers include its transformed (off-screen) box in the document's
       scrollable overflow, which otherwise creates ~260px of invisible
       horizontal scroll on every page below the 768px breakpoint. Set on
       both html and body since which one is the actual scrolling root
       varies by which element declares a non-default overflow first. */
    overflow-x: hidden;
}

/* ─── Base reset (dark pages only — light pages use Tailwind) ──────────────── */
.dark-page {
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.6;
}
.dark-page h1,
.dark-page h2,
.dark-page h3 {
    font-family: var(--font-display);
    font-weight: 800;
}

/* ─── Shared Components ─────────────────────────────────────────────────────── */
/* M164: formalized as the app's one real elevated-surface component — until
   now this had zero actual adoption anywhere (docs/design-system.md's "4
   pages" was a stale false-positive substring match), despite at least 11
   different css/*.css files independently hand-duplicating this exact
   recipe under their own class names. Added the M159 shadow token it was
   missing (a bordered box with no shadow doesn't read as "raised"), then
   started adopting it for real — see journal.css/listening-pilot.css/
   tool-6-conversation-coach.css/tool-7-oral-test.css for the pattern:
   compose `class="s-card <page-specific-modifier>"` when a page has real
   extra behavior (its own descendant selectors), or just `class="s-card"`
   when it doesn't. */
.s-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 24px;
    box-shadow: var(--shadow-s);
}

.s-btn {
    font-family: var(--font-body);
    font-weight: 600;
    border: none;
    border-radius: var(--radius);
    padding: 12px 24px;
    cursor: pointer;
    transition: all var(--transition);
    font-size: 15px;
    line-height: 1;
    /* .s-btn is used on <a> tags throughout the app (quick-links,
       assessment-guide's CTAs, etc.) as often as on real <button>s — a
       no-op for buttons, but without it every anchor usage silently shows
       a browser-default underline nothing else on the page has. */
    text-decoration: none;
}
.s-btn-primary {
    background: var(--accent);
    color: #0d1117;
}
.s-btn-primary:hover {
    filter: brightness(1.1);
    transform: translateY(-1px);
}
.s-btn-ghost {
    background: var(--surface-2);
    color: var(--text);
    border: 1px solid var(--border);
}
.s-btn-ghost:hover {
    border-color: var(--accent);
    color: var(--accent);
}
.s-btn-teal {
    background: var(--teal);
    color: #0d1117;
}
.s-btn-teal:hover {
    filter: brightness(1.1);
    transform: translateY(-1px);
}
.s-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none !important;
    filter: none !important;
}
/* M165: compact size modifier — additive, not a redesign, same pattern as
   .spinner-lg (M156). admin.html's action buttons were their own,
   independently-hand-styled compact button family (.action-btn,
   css/admin.css) before this; folded onto .s-btn/.s-btn-ghost + this
   modifier instead of keeping a second button system. */
.s-btn-sm {
    padding: .3rem .7rem;
    font-size: .8rem;
    border-radius: 8px;
}

.s-feedback {
    border-radius: var(--radius);
    padding: 16px 20px;
    font-size: 14px;
    line-height: 1.7;
}
.s-feedback-ai {
    background: var(--purple-soft);
    border-left: 3px solid var(--purple);
}
.s-feedback-good {
    background: var(--green-soft);
    border-left: 3px solid var(--green);
}
.s-feedback-warn {
    background: var(--accent-soft);
    border-left: 3px solid var(--accent);
}
.s-feedback-error {
    background: var(--red-soft);
    border-left: 3px solid var(--red);
}

.empty-state {
    text-align: center;
    padding: 2.5rem 1.75rem;
    color: var(--text-muted);
    font-size: .9rem;
}

.s-progress-bar {
    background: var(--surface-2);
    border-radius: 99px;
    height: 6px;
    overflow: hidden;
}
.s-progress-fill {
    height: 100%;
    background: var(--accent);
    border-radius: 99px;
    transition: width 0.4s ease;
}

.s-tag {
    display: inline-block;
    font-size: 12px;
    font-weight: 600;
    font-family: var(--font-display);
    padding: 4px 10px;
    border-radius: 99px;
    letter-spacing: 0.03em;
}
.s-tag-teal {
    background: var(--teal-soft);
    color: var(--teal);
}
.s-tag-green {
    background: var(--green-soft);
    color: var(--green);
}
.s-tag-red {
    background: var(--red-soft);
    color: var(--red);
}
.s-tag-accent {
    background: var(--accent-soft);
    color: var(--accent);
}
.s-tag-purple {
    background: var(--purple-soft);
    color: var(--purple);
}

.voice-btn {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    border: 2px solid var(--border);
    background: var(--surface-2);
    cursor: pointer;
    transition: all 0.2s;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 22px;
    flex-shrink: 0;
}
.voice-btn.recording {
    border-color: var(--red);
    background: var(--red-soft);
    animation: pulse-red 1s infinite;
}
@keyframes pulse-red {
    0%,
    100% {
        box-shadow: 0 0 0 0 var(--red);
    }
    50% {
        box-shadow: 0 0 0 8px transparent;
    }
}

.s-input {
    background: var(--surface-2);
    border: 1.5px solid var(--border);
    color: var(--text);
    border-radius: var(--radius);
    padding: 0.65rem 1rem;
    font-size: 1rem;
    font-family: var(--font-body);
    transition: border-color var(--transition);
    width: 100%;
    box-sizing: border-box;
}
.s-input:focus {
    outline: none;
    border-color: var(--accent);
}

/* ─── Sidebar Mega-Menu ─────────────────────────────────────────────────────── */
#app-shell {
    display: flex;
    min-height: 100vh;
}

/* Sidebar */
/* Full-page menu, both desktop and mobile — an on-demand overlay you open
   with the hamburger, not a permanently-visible rail. #main-content is
   always full-width; this covers the entire viewport when `.open`. */
#sidebar {
    width: 100vw;
    background: var(--surface);
    display: flex;
    flex-direction: column;
    position: fixed;
    inset: 0;
    z-index: 200;
    overflow-y: auto;
    overflow-x: hidden;
    transform: translateX(-100%);
    transition: transform 0.3s ease;
}
#sidebar.open {
    transform: translateX(0);
}
#sidebar::-webkit-scrollbar {
    width: 4px;
}
#sidebar::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: 4px;
}

.sidebar-logo {
    padding: 1.25rem 1rem 1.25rem 68px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 0.75rem;
    text-decoration: none;
    flex-shrink: 0;
    /* 68px clears the fixed #sidebar-toggle (44px wide, 12px inset) so the
       ✕/☰ button never sits on top of the logo when the menu is open. */
}
.sidebar-logo-icon {
    font-size: 1.6rem;
    flex-shrink: 0;
}
.sidebar-logo-text {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 1rem;
    color: var(--text);
    line-height: 1.2;
}
.sidebar-logo-sub {
    font-size: 0.72rem;
    color: var(--text-muted);
    margin-top: 1px;
}

/* Full-page menu content is centered with room to breathe — --sidebar-w
   (admin-configurable, "Menu Content Width") caps the grid's line length on
   wide desktop screens instead of letting tiles stretch edge-to-edge. */
.sidebar-section {
    width: 100%;
    max-width: var(--sidebar-w);
    box-sizing: border-box;
    margin: 0 auto;
    padding: 1rem 1.25rem;
    border-top: 1px solid var(--border);
}
.sidebar-section:first-of-type {
    border-top: none;
}

/* Usually a real <button> (the mobile accordion toggle) — reset button
   chrome so it still reads as a plain section label. A category with
   exactly one destination (.sidebar-section-single) reuses this same class
   on a plain, non-interactive <div> instead: no accordion, nothing to
   toggle, so cursor:default overrides the button default below. */
.sidebar-section-label {
    width: 100%;
    background: none;
    border: none;
    font: inherit;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-dim);
    padding: 0.3rem 0.2rem 0.6rem;
    display: flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
    text-align: left;
}
.sidebar-section-single .sidebar-section-label {
    cursor: default;
}
.sidebar-section-label .chevron {
    display: inline-block;
    transition: transform 0.25s ease;
}
.sidebar-section.open .sidebar-section-label .chevron {
    transform: rotate(90deg);
}

/* Animated expand/collapse (mobile accordion) via the grid-template-rows
   0fr->1fr trick — animates smoothly to content's real height without
   needing JS to measure scrollHeight. Desktop overrides this open always
   (see the >768px block below) since the accordion is a mobile-only
   space-saving device, not a desktop feature. */
.sidebar-section-body {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.3s ease;
}
.sidebar-section.open .sidebar-section-body {
    grid-template-rows: 1fr;
}
.sidebar-section-body > .sidebar-grid {
    overflow: hidden;
    min-height: 0;
}

/* Plain text-link list, not a card grid. auto-fit (not auto-fill) so a
   1-link section renders one short, un-stretched line instead of a
   stretched block — same lesson as the old card-grid's M121 fix, applied
   to text rows instead of square tiles. A definite max (220px, not 1fr) is
   required for the same reason: auto-fit computes track COUNT from
   minmax()'s max value once it's a definite length, so a small, deliberate
   max (not just "whatever looks OK") is what makes a 9-link section
   actually flow into multiple columns instead of stacking as one. */
.sidebar-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 220px));
    justify-content: start;
    gap: 0.5rem 1.5rem;
}

.sidebar-link {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.45rem 0;
    color: var(--text-muted);
    text-decoration: none;
    font-size: 0.88rem;
}
.sidebar-link:hover {
    color: var(--text);
}
.sidebar-link.active {
    color: var(--accent);
    font-weight: 700;
}
.sidebar-link .icon {
    font-size: 1.05rem;
    line-height: 1;
    width: 1.3rem;
    text-align: center;
    flex-shrink: 0;
}
.sidebar-link .label {
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sidebar-link .badge {
    font-size: 0.6rem;
    font-weight: 700;
    background: var(--accent-soft);
    color: var(--accent);
    border: 1px solid var(--accent);
    padding: 1px 5px;
    border-radius: 99px;
    opacity: 0.85;
    letter-spacing: 0.03em;
    flex-shrink: 0;
}
.sidebar-link .badge.nav-count {
    background: var(--surface-2);
    color: var(--text-dim);
    border-color: var(--border);
    font-weight: 600;
}

/* User / logout area */
/* Pushes the whole mobile footer block to the bottom of #sidebar's flex
   column — moved here from .sidebar-user itself once that got nested one
   level deeper inside this wrapper (M130); auto margins only push within
   their own direct flex parent. */
.sidebar-footer-mobile {
    margin-top: auto;
}
.sidebar-user {
    padding: 0.75rem 1rem;
    border-top: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-shrink: 0;
}
/* A <button> now (opens the profile popover, M140), not a plain <span> —
   reset button chrome, matching .navbar-username's own reset. */
.sidebar-user-name {
    background: none;
    border: none;
    font: inherit;
    font-size: 0.85rem;
    color: var(--text-muted);
    flex: 1;
    text-align: left;
    cursor: pointer;
    padding: 0;
}
.sidebar-user-name:hover {
    color: var(--text);
}
.sidebar-logout {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-dim);
    font-size: 1rem;
    padding: 0.2rem 0.4rem;
    border-radius: 6px;
    transition: color 0.2s;
}
.sidebar-logout:hover {
    color: var(--red);
}
.sidebar-version {
    text-align: center;
    font-size: 0.67rem;
    color: var(--text-dim);
    padding: 0.2rem 1rem 0.6rem;
    letter-spacing: 0.05em;
    opacity: 0.55;
    user-select: all;
}
.navbar-version {
    font-size: 0.67rem;
    color: var(--text-dim);
    letter-spacing: 0.05em;
    opacity: 0.55;
    user-select: all;
    white-space: nowrap;
    flex-shrink: 0;
}

/* Skeleton loader for sidebar */
.sidebar-skel-line {
    height: 10px;
    background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 50%, var(--surface-2) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
    border-radius: 6px;
    margin-bottom: 10px;
}
@keyframes shimmer {
    0% {
        background-position: 200% center;
    }
    100% {
        background-position: -200% center;
    }
}

/* Persistent nav bar — top on desktop, bottom on mobile (media query
   below). Sits above #sidebar (z-index) so its toggle (☰/✕) stays reachable
   to contract the full-page menu back down while it's open, not just to
   open it. 60px height is shared with #main-content's offset below. */
#nav-bar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 60px;
    padding: 0 max(12px, env(safe-area-inset-left)) 0 max(16px, env(safe-area-inset-left));
    padding-top: env(safe-area-inset-top);
    z-index: 250;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    /* flex-start (not space-between) so #nav-bar-controls sits right next to
       the logo, not pushed toward the middle — the toggle button gets its
       own margin-left:auto below to stay pinned at the far end regardless. */
    justify-content: flex-start;
    gap: 1.25rem;
    box-shadow: var(--shadow-s);
    /* Scroll sideways rather than hide things (M340). M323 hid the whole
       cluster under 1280px and M326 still dropped the summary stats and the
       brand wordmark under 768px — both were choosing WHAT to sacrifice. A
       scrollable bar sacrifices nothing: everything stays reachable, the
       learner just slides to it. The scrollbar itself is hidden because a
       visible one across 60px of chrome reads as a defect, not an affordance. */
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;          /* Firefox */
    -ms-overflow-style: none;       /* old Edge */
    overscroll-behavior-x: contain; /* don't chain into a page/history swipe */
}
#nav-bar::-webkit-scrollbar { display: none; }
/* Nothing in a scrolling row may be squeezed: flex would rather shrink a child
   than overflow, which is exactly the truncation this replaces ("French S…"). */
#nav-bar > * { flex: none; }

/* Desktop-only cluster (account controls formerly at the bottom of the
   full-page menu) — hidden on mobile, where .sidebar-footer-mobile (inside
   the menu itself) covers the same controls; a 60px-tall bottom bar has no
   room for theme swatches + voice picker + credits + logout all at once. */
.nav-bar-controls {
    display: flex;
    align-items: center;
    gap: 1rem;
    /* was overflow:hidden — that is what silently ATE controls instead of
       letting them overflow, which is the M323 bug. The bar scrolls now, so
       the cluster can simply be its natural width. */
    overflow: visible;
}

/* ── Compact bar chips (M326) ─────────────────────────────────────────────
   One button each for voice and theme, replacing eight pills and five
   swatches inline. Both open a .picker-popover holding the full picker, so
   the pickers themselves are unchanged and .voice-swatch/.theme-swatch stay
   the single source of active-state truth. */
.navbar-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.28rem 0.5rem;
    background: var(--surface-2, rgba(255, 255, 255, 0.06));
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font: inherit;
    font-size: 0.75rem;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
}
.navbar-chip:hover {
    border-color: var(--accent, var(--text-muted));
}
.navbar-chip-caret {
    font-size: 0.6rem;
    color: var(--text-muted);
}
.navbar-chip-label {
    max-width: 7ch;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Mirrors the selected theme's own swatch colour via the same [data-theme]
   attribute the .theme-swatch rules already key off. */
/* A stronger border than .theme-swatch uses: at 14px on a same-family
   surface (dark's #1a2233 against the bar) the fill alone is invisible and
   the chip reads as an empty pill — which is the exact discoverability
   failure the chips exist to fix. The 🎨 beside it carries the meaning; the
   dot only has to show WHICH theme. */
.theme-swatch-preview {
    width: 13px;
    height: 13px;
    border-radius: 50%;
    border: 1px solid var(--text-muted);
    display: inline-block;
    flex: none;
}

.picker-popover {
    width: 232px;
}
.picker-group + .picker-group {
    margin-top: 0.6rem;
}
.picker-group-label {
    font-size: 0.62rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    margin-bottom: 0.3rem;
}
.picker-group-items {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
}
/* Inside the popover the theme swatches get room for a label, unlike the
   bare colour circles the mobile footer still uses. */
.picker-popover .theme-swatch {
    width: auto;
    height: auto;
    border-radius: 999px;
    padding: 0.25rem 0.55rem;
}
.picker-popover .theme-swatch-name {
    font-size: 0.7rem;
}
/* A <button> now (opens the profile popover), not a plain <span> — reset
   button chrome so it still just reads as text. */
.navbar-username {
    background: none;
    border: none;
    font: inherit;
    font-size: 0.82rem;
    color: var(--text-muted);
    white-space: nowrap;
    cursor: pointer;
    padding: 0;
    transition: color 0.15s;
    /* Also hosts the avatar + streak/activity summary (M201) — was
       plain text before, now a small flex row so those fit inline
       without a separate button/layout. */
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.navbar-username:hover {
    color: var(--text);
}
.profile-avatar-sm {
    width: 24px;
    height: 24px;
    font-size: 0.7rem;
}
/* Streak / today / total cluster next to the navbar avatar (M201) — reads
   left-to-right as "today's activity, lifetime total, current streak" via
   title tooltips; kept terse (no labels) since #nav-bar is only 60px tall
   and shared with the theme/voice pickers and logout button. */
/* One status cluster, not three floating numbers (M-H5). Three separate
   items with three separate tooltips read as noise in a bar that is already
   dense; bounded and hairline-separated they read as one figure with three
   parts, which is what they are — the streak only means something beside
   today's count. Deliberately not a card: it is a readout, not an object. */
.navbar-summary-stats {
    display: inline-flex;
    align-items: center;
    gap: 0;
    font-size: 0.76rem;
    color: var(--text-muted);
    white-space: nowrap;
    border: 1px solid var(--rule, var(--border));
    border-radius: 999px;
    padding: 0.12rem 0.1rem;
}
.navbar-stat {
    padding: 0 0.42rem;
    display: inline-flex;
    align-items: center;
    gap: 0.22rem;
}
.navbar-stat + .navbar-stat {
    border-left: 1px solid var(--border);
}
.navbar-summary-stats .navbar-stat-num {
    font-weight: 600;
    color: var(--text);
}
.navbar-admin-link {
    font-size: 1.1rem;
    text-decoration: none;
    line-height: 1;
    opacity: 0.85;
    transition: opacity 0.15s;
}
.navbar-admin-link:hover {
    opacity: 1;
}
.profile-popover-badges {
    display: flex;
    gap: 0.4rem;
    margin-bottom: 0.6rem;
}
.profile-badge {
    font-size: 0.68rem;
    font-weight: 600;
    background: var(--surface-2);
    color: var(--text-muted);
    border: 1px solid var(--border);
    border-radius: 99px;
    padding: 0.15rem 0.55rem;
}
.profile-popover-link {
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    font: inherit;
    font-size: 0.78rem;
    color: var(--text-muted);
    text-decoration: none;
    padding: 0.45rem 0.2rem;
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}
.profile-popover-link:hover {
    background: var(--surface-2);
    color: var(--text);
}
.profile-popover-logout {
    color: var(--red);
    border-top: 1px solid var(--border);
    margin-top: 0.3rem;
    padding-top: 0.55rem;
}

/* ─── Profile popover: view/edit (M140) ─────────────────────────────────────── */
.profile-popover-wide {
    width: 280px;
}
.profile-popover-loading {
    color: var(--text-dim);
    font-size: 0.78rem;
    padding: 0.4rem 0.2rem;
}
.profile-popover-header {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    margin-bottom: 0.6rem;
}
.profile-avatar {
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
}
.profile-avatar-fallback {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--accent-soft);
    color: var(--accent);
    font-weight: 700;
}
.profile-avatar-lg {
    width: 42px;
    height: 42px;
    font-size: 1.1rem;
}
.profile-popover-name {
    font-weight: 700;
    font-size: 0.9rem;
    color: var(--text);
    margin-bottom: 0.25rem;
}
.profile-popover-bio {
    font-size: 0.78rem;
    color: var(--text-muted);
    line-height: 1.4;
    margin: 0 0 0.65rem;
}
.profile-popover-stats {
    display: flex;
    gap: 1rem;
    margin-bottom: 0.65rem;
    padding: 0.55rem 0;
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
}
.profile-stat {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
}
.profile-stat-num {
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--text);
}
.profile-stat-label {
    font-size: 0.66rem;
    color: var(--text-dim);
}
.profile-edit-field {
    margin-bottom: 0.6rem;
}
.profile-edit-avatar-row {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    margin-bottom: 0.3rem;
}
.profile-edit-avatar-controls {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}
.profile-edit-avatar-url-details summary {
    cursor: pointer;
    font-size: 0.7rem;
    color: var(--text-dim);
    margin-bottom: 0.3rem;
}
.profile-edit-avatar-url-details input {
    width: 100%;
    box-sizing: border-box;
    padding: 0.4rem 0.55rem;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--text);
    font: inherit;
    font-size: 0.78rem;
}
.profile-edit-field label {
    display: block;
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--text-muted);
    margin-bottom: 0.2rem;
}
.profile-edit-hint {
    font-weight: 400;
    color: var(--text-dim);
}
.profile-edit-field input,
.profile-edit-field textarea {
    width: 100%;
    box-sizing: border-box;
    padding: 0.4rem 0.55rem;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--text);
    font: inherit;
    font-size: 0.78rem;
    resize: vertical;
}
.profile-edit-field input:focus,
.profile-edit-field textarea:focus {
    outline: none;
    border-color: var(--accent);
}
.profile-edit-alert {
    font-size: 0.72rem;
    color: var(--red);
    background: var(--red-soft);
    border: 1px solid var(--red);
    border-radius: 6px;
    padding: 0.4rem 0.55rem;
    margin-bottom: 0.5rem;
}
.profile-edit-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
}
.profile-edit-actions .profile-popover-link {
    width: auto;
}
.ub-badge {
    background: var(--surface-2);
    border: 1px solid var(--border);
    color: var(--text-muted);
    border-radius: 99px;
    padding: 0.3rem 0.7rem;
    font-size: 0.78rem;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    transition: border-color 0.15s, color 0.15s;
}
.ub-badge:hover {
    border-color: var(--accent);
    color: var(--accent);
}
/* Floating detail panel — appended to <body>, positioned in JS (fixed
   under the badge) rather than as a normal in-flow child, since #nav-bar's
   fixed height/overflow would otherwise clip an expanded block. */
.ub-popover {
    display: none;
    position: fixed;
    z-index: 260;
    width: 220px;
    max-height: calc(100vh - 24px);
    overflow-y: auto;
    padding: 0.7rem 0.85rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: var(--shadow-m);
    font-size: 0.72rem;
}
.ub-popover.open {
    display: block;
}

/* First-visit onboarding tip (M193) — js/onboarding.js. Same floating-
   panel recipe as .ub-popover (appended to <body>, JS-positioned), with
   an accent border instead of the default one to read as "look at me,
   it's new" rather than blend in like a standard popover. */
.onboarding-tip {
    display: none;
    position: fixed;
    z-index: 270;
    max-width: 260px;
    padding: 0.75rem 0.85rem;
    background: var(--surface);
    border: 1px solid var(--accent);
    border-radius: 10px;
    box-shadow: var(--shadow-m);
    font-size: 0.8rem;
}
.onboarding-tip.open {
    display: block;
}
.onboarding-tip-text {
    color: var(--text);
    line-height: 1.45;
    margin-bottom: 0.55rem;
}
.nav-bar-brand {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    text-decoration: none;
    overflow: hidden;
}
.nav-bar-brand .sidebar-logo-icon {
    font-size: 1.3rem;
    flex-shrink: 0;
}
.nav-bar-brand .sidebar-logo-text {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 0.95rem;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Hamburger toggle — 44px is the touch-target floor most guidelines
   recommend. A flex child of #nav-bar now, not independently positioned. */
#sidebar-toggle {
    display: flex;
    flex-shrink: 0;
    margin-left: auto; /* pinned at the far end regardless of #nav-bar-controls' width */
    /* Pinned to the right edge of the VIEWPORT, not to the end of the scroll
       content (M340). In a horizontally scrolling bar `margin-left:auto` alone
       parks the menu button past the fold, so opening the menu would need a
       sideways scroll first — the one control that must never be more than a
       tap away. `sticky` holds it while everything else slides under it, and
       the background is what stops that content showing through. */
    position: sticky;
    right: 0;
    box-shadow: -10px 0 10px -8px rgba(0, 0, 0, 0.2);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 12px;
    width: 44px;
    height: 44px;
    cursor: pointer;
    align-items: center;
    justify-content: center;
    font-size: 1.25rem;
    color: var(--text);
    transition: background var(--transition), color var(--transition), border-color var(--transition), transform 0.15s ease;
}
#sidebar-toggle:active {
    transform: scale(0.92);
}
/* Modern-browser affordance: the toggle recolors while the menu is open, no
   JS icon-swap needed. Degrades harmlessly (just stays neutral) on browsers
   without :has() support. */
body:has(#sidebar.open) #sidebar-toggle {
    background: var(--accent-soft);
    color: var(--accent);
    border-color: var(--accent);
}

/* Overlay — kept in the render tree (not display:none) so it can fade
   in/out instead of snapping, and pointer-events gates click-to-close. */
#sidebar-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    z-index: 150;
    backdrop-filter: blur(2px);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
}
#sidebar-overlay.open {
    opacity: 1;
    pointer-events: auto;
}

/* Main content — always full-width now that #sidebar is an overlay, not a
   rail. Every page's own header/hero starts flush at the top with no
   awareness of the fixed bar — without this padding, it sits directly on
   top of the page title on every single tool page. */
#main-content {
    flex: 1;
    min-width: 0;
    padding-top: 60px;
}

/* ─── Breakpoint convention (M151): 768px is this app's one canonical
   breakpoint — CSS custom properties can't be referenced inside @media
   conditions in any shipping browser, so this is a literal-value
   convention, not a real token; tools/design-lint.js (M158) flags any
   other pixel value used for a mobile/desktop split. Mobile queries use
   `max-width: 768px`; a query for its exact desktop complement uses
   `min-width: 769px` (one pixel higher, not a typo — see line ~1053
   below) so the two never overlap at exactly 768px. ───────────────────── */
/* ─── Mobile — bar moves to the bottom (thumb reach), main content's
   reserved space moves with it ─────────────────────────────────────────── */
@media (max-width: 768px) {
    /* Single column on mobile (no room for a second) — plain divided rows,
       like a standard mobile nav sheet, instead of the desktop's flowing
       multi-column text list. */
    .sidebar-grid {
        grid-template-columns: 1fr;
    }
    .sidebar-link {
        padding: 0.85rem 0;
        font-size: 0.95rem;
        border-bottom: 1px solid var(--border);
    }
    .sidebar-link:last-child {
        border-bottom: none;
    }
    .sidebar-link .icon {
        font-size: 1.2rem;
    }
    #nav-bar {
        top: auto;
        bottom: 0;
        border-bottom: none;
        border-top: 1px solid var(--border);
        padding-top: 0;
        padding-bottom: env(safe-area-inset-bottom);
        box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.25);
    }
    #main-content {
        padding-top: 0;
        padding-bottom: 60px;
    }
    .s-btn {
        padding: 14px 24px;
        min-height: 44px;
    }
    /* Real, duplicated (byte-identical or near-identical) small interactive
       recipes across 2+ page-CSS files (M189) — a genuine mobile tap-target
       gap `.s-btn` alone doesn't cover, since these are page-specific
       classes with their own base rule, not `.s-btn` instances. Only
       classes confirmed literally duplicated across multiple files belong
       here; a page-unique small control gets its own `@media` block in
       that page's own CSS instead (see docs/design-system.md's breakpoint
       convention) — this is not a catch-all for every small button. */
    .accent-btn, .hist-audio-btn, .speaker-btn, .mic-btn, .send-btn,
    .mode-tab, .speed-pill {
        min-height: 44px;
    }
}

/* ── The account cluster needs a real desktop, not merely 'not a phone' ──
   M323. This lived in the max-width:768px block above, which meant every
   width from 769px up got the full #nav-bar-controls set: usage badge, 5
   theme swatches, 8 voice pills, TTS controls, admin gear, avatar + 3
   stats, logout and the version string.

   Measured, the cluster needs 1108px on its own — about 1290px once the
   brand and the menu button are counted. Below that the voice picker
   wraps INSIDE a 60px bar that is `overflow: hidden`, so the extra rows
   are not merely ugly, they are unreachable: at 834px it grew to 216px
   tall and 8 rows, spilling over the page content, with Florian, Ambre
   and Marie clipped out of existence entirely. An iPad in portrait could
   not select five of the eight voices.

   It grew into this. The bar was fine with four voices; M295-M321 took it
   to eight and nothing re-checked the tablet range.

   1279px is the exact non-overlapping complement of the 1280px rule that
   reveals .sidebar-footer-mobile — see the breakpoint convention in
   docs/design-system.md. Nothing is lost below it: that footer carries
   the same theme picker, voice picker, gear, logout, stats, its own AI
   credits strip and `v{version} · {build}`. */
/* M326 removed the `.nav-bar-controls { display: none }` that used to live
   here. M323 added it because eight voice pills plus five theme swatches ran
   the bar to ~1244px and wrapped, cutting controls off on tablets. Hiding the
   cluster fixed the overflow but left the tablet/phone bar holding a brand and
   a hamburger and nothing else — so nothing signalled that theming, voice or
   settings existed at all. Discoverability, not layout, was the real cost.

   The chips (navVoiceChipHtml/navThemeChipHtml in nav.js) collapse those
   thirteen controls into two ~146px buttons, so the cluster now fits at every
   width and stays visible. Below 768px the summary stats and brand wordmark
   drop out instead — see the rule further down — which is a much smaller loss
   than the whole cluster. */
@media (max-width: 767px) {
    /* M326 hid the summary stats and the brand wordmark here. M340 restored
       both: the bar scrolls now, so there is no longer a width at which
       something has to be sacrificed — which is the whole point of scrolling
       rather than truncating. Only the spacing tightens, so more of the row
       fits before any sliding is needed. */
    #nav-bar {
        gap: 0.6rem;
    }
    .nav-bar-controls {
        gap: 0.45rem;
    }
}

/* The accordion (collapsed-by-default categories) is a mobile space-saving
   device only — desktop already shows every section fully, laid out in
   columns, so every section stays permanently open and the chevron/click
   affordance (still attached in JS, just inert here) is hidden.
   769px (not 768px) is deliberate — see the breakpoint-convention banner
   above: this is max-width:768px's exact non-overlapping complement. */
@media (min-width: 769px) {
    .sidebar-section-label {
        cursor: default;
    }
    .sidebar-section-label .chevron {
        display: none;
    }
    .sidebar-section-body {
        grid-template-rows: 1fr !important;
    }
}

/* Kept at 1280px, but the reasoning changed in M326. This used to be the
   exact complement of a `.nav-bar-controls { display: none }` rule, because
   below 1280px the menu was the ONLY place these controls existed — so the
   two breakpoints had to move together or a band of widths would have had no
   voice picker at all.

   That rule is gone: the bar now carries voice/theme chips at every width.
   This footer therefore duplicates them below 1280px, which is deliberate and
   harmless — it lives inside a menu you have to open, and it is the only
   remaining home for the AI-credits strip and the `v{version} · {build}`
   string that M326 removed from the bar. The two breakpoints are now
   independent; this one can move without touching the bar. */
@media (min-width: 1280px) {
    .sidebar-footer-mobile {
        display: none;
    }
}

/* ── Collapsible page headers (M202) — generic toggle, driven entirely by
   nav.js's initHeaderCollapse(): works on both .page-header (23 tool pages,
   title + one-line subtitle) and drills' own .bg-navy-900 header (multi-
   user bar + mode-bar) since both just mark their collapsible region with
   .hdr-collapse-body and their toggle with .hdr-collapse-toggle inside a
   .hdr-collapsible wrapper. Same grid-template-rows 0fr/1fr recipe as the
   sidebar's own mobile accordion (.sidebar-section-body above) — reused,
   not reinvented. Server-rendered markup always starts with .open already
   present so there's no flash-of-collapsed before JS runs; JS only ever
   removes .open, for a user who collapsed it on a previous visit. */
/* M328: a labelled pill beside the title, not a 26px circle at the far edge.
   The word is the whole point — a bare chevron does not say WHAT collapses,
   and this one persists its state across visits, so a user who pressed it by
   accident had no way to understand why the page looked different. Auto width
   (it holds "Show less"/"Show more") and a real touch height, since it is now
   the only placement at every width. */
.hdr-collapse-toggle {
    background: rgba(127, 127, 127, 0.16);
    border: none;
    color: inherit;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    min-height: 30px;
    padding: 0.25rem 0.6rem;
    border-radius: 999px;
    flex-shrink: 0;
    font: inherit;
    font-size: 0.72rem;
    line-height: 1;
    transition: background 0.15s;
}
.hdr-toggle-label {
    white-space: nowrap;
}

.hdr-collapse-toggle:hover {
    background: rgba(127, 127, 127, 0.3);
}
.hdr-collapse-toggle .chevron {
    display: inline-block;
    transition: transform 0.25s ease;
}
.hdr-collapsible.open .hdr-collapse-toggle .chevron {
    transform: rotate(90deg);
}
.hdr-collapse-body {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.3s ease;
}
.hdr-collapsible.open .hdr-collapse-body {
    grid-template-rows: 1fr;
}
.hdr-collapse-body > .hdr-collapse-inner {
    overflow: hidden;
    min-height: 0;
}
/* flex-start, not space-between (M328). space-between is what put ~1,220px
   of desktop between the title and the control that collapses it, which is
   what made the control read as unowned. It now sits directly beside the
   title it belongs to. */
.page-header-title-row {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 0.75rem;
}
.page-header-title-row h1 {
    margin: 0;
}
/* Drills' compact collapsed-state title (views/drill/_header.php) — the
   real <h1> lives inside .hdr-collapse-body, so this stand-in label is
   only ever shown while collapsed; CSS-driven off the same .open class
   used everywhere else here, not JS-toggled separately. */
.hdr-collapsible.open .hdr-collapsed-label {
    display: none;
}

/* Pulse animation reuse */
@keyframes pop {
    0% {
        transform: scale(0.85);
        opacity: 0;
    }
    60% {
        transform: scale(1.08);
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}
.pop {
    animation: pop 0.25s ease forwards;
}

@keyframes slidein {
    0% {
        opacity: 0;
        transform: translateY(8px);
    }
    100% {
        opacity: 1;
        transform: translateY(0);
    }
}
.slidein {
    animation: slidein 0.3s ease forwards;
}

/* Spinner */
.spinner {
    display: inline-block;
    width: 18px;
    height: 18px;
    border: 2px solid transparent;
    border-top-color: currentColor;
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
    vertical-align: middle;
}
/* M156: explicit large-spinner modifier (class="spinner spinner-lg") for
   long-running states (e.g. AI grading) that want a more prominent,
   slower-spinning indicator than the default inline 18px one — a real,
   intentional size difference, not drift, so it's named and shared
   rather than deleted. Combine with .spinner; this only overrides what
   actually differs (size/border-width/speed), not color/layout, which
   stay page-local since those genuinely vary by context. */
.spinner-lg {
    width: 32px;
    height: 32px;
    border-width: 3px;
    animation-duration: 0.8s;
}
@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

/* AI loading shimmer */
.ai-loading {
    background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 50%, var(--surface-2) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
    border-radius: var(--radius);
    height: 60px;
}
@keyframes shimmer {
    0% {
        background-position: 200% 0;
    }
    100% {
        background-position: -200% 0;
    }
}

/* ─── Sidebar footer tools (TTS stop + theme picker) ───────────────────────── */
.sidebar-footer-tools {
    display: flex;
    flex-direction: column;
    gap: .45rem;
    padding: .45rem .85rem .2rem;
}
.nav-tts-stop,
.nav-tts-pause {
    display: none;
    align-items: center;
    justify-content: center;
    gap: .35rem;
    width: 100%;
    padding: .36rem .75rem;
    border-radius: 8px;
    font-size: .75rem;
    font-weight: 600;
    cursor: pointer;
    border: 1.5px solid var(--red);
    background: transparent;
    color: var(--red);
    transition: background .15s, opacity .15s;
}
.nav-tts-pause {
    border-color: var(--border);
    color: var(--text-muted);
}
.nav-tts-stop:hover { background: var(--red-soft); }
.nav-tts-pause:hover { background: var(--surface-2); color: var(--text); }

/* Compact icon-only variant for the desktop nav-bar cluster — the mobile
   footer's full-width text-label buttons above would blow out the bar's
   slim height and crowd the theme/voice pickers already there. */
.navbar-tts-btn {
    width: 28px;
    height: 28px;
    padding: 0;
    border-radius: 50%;
    font-size: .85rem;
    flex-shrink: 0;
}

/* ─── Theme swatches ───────────────────────────────────────────────────────── */
.theme-picker {
    display: flex;
    align-items: center;
    gap: .45rem;
    padding: .2rem 0;
}
.theme-swatch {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 2px solid transparent;
    cursor: pointer;
    transition: transform .15s, border-color .15s;
    flex-shrink: 0;
    padding: 0;
}
.theme-swatch:hover { transform: scale(1.2); }
.theme-swatch.active { border-color: var(--text); transform: scale(1.15); }
.theme-swatch[data-theme="dark"],   .theme-swatch-preview[data-theme="dark"]   { background: #1a2233; }
.theme-swatch[data-theme="light"],  .theme-swatch-preview[data-theme="light"]  { background: #e8ecf0; border-color: #ccc; }
.theme-swatch[data-theme="ocean"],  .theme-swatch-preview[data-theme="ocean"]  { background: #0a3060; }
.theme-swatch[data-theme="forest"], .theme-swatch-preview[data-theme="forest"] { background: #0d2e10; }
.theme-swatch[data-theme="app"],    .theme-swatch-preview[data-theme="app"]    { background: #2f6fed; }

/* Inside the popover the option is a dot + a readable label, so the button
   itself must NOT take the theme colour as its background — dark text on
   #1a2233 is unreadable. `.picker-popover .theme-swatch.theme-option` is
   (0,3,0) and beats the (0,2,0) colour rules above without !important. */
.picker-popover .theme-swatch.theme-option {
    background: none;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--text);
}
.picker-popover .theme-swatch.theme-option.active {
    border-color: var(--accent, var(--text));
}

/* ─── Voice picker (default TTS voice; per-tool override lives in each tool) ── */
.voice-picker {
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .15rem 0 .3rem;
}
.voice-swatch {
    font-size: .7rem;
    font-weight: 600;
    padding: .2rem .55rem;
    border-radius: 99px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: background .15s, color .15s, border-color .15s;
}
.voice-swatch:hover { border-color: var(--teal); color: var(--teal); }
.voice-swatch.active { background: var(--teal); border-color: var(--teal); color: #fff; }
/* Four voices in one row is tight on a phone; let them wrap rather than
   overflow the sidebar footer. */
.voice-picker { flex-wrap: wrap; row-gap: .3rem; }
/* Piper voices run on the device instead of streaming a pre-made file, so
   they get their own accent — the distinction matters when one of them is
   downloading or unavailable. A dot rather than a label keeps the pills the
   same size as Florian/Ambre. */
.voice-swatch.voice-piper::before {
    content: '';
    display: inline-block;
    width: .34em;
    height: .34em;
    margin-right: .32em;
    vertical-align: .08em;
    border-radius: 50%;
    background: currentColor;
    opacity: .55;
}
.voice-swatch.voice-piper:hover { border-color: var(--gold, #f0a500); color: var(--gold, #f0a500); }
.voice-swatch.voice-piper.active { background: var(--gold, #f0a500); border-color: var(--gold, #f0a500); color: #1a1205; }
/* Set while a Piper model is downloading or synthesizing, so the pill shows
   the engine is busy instead of appearing to have ignored the click. */
.voice-swatch.voice-busy { opacity: .6; cursor: progress; }

/* Supertonic voices get their own marker. With eight pills in one row, three
   undifferentiated groups read as one long undifferentiated list — the marker
   is what lets the eye find "the studio pair" or "the local pair" at a glance
   instead of reading eight names. A hollow ring rather than Piper's filled dot,
   so the two are distinguishable without relying on colour alone. */
.voice-swatch.voice-supertonic::before {
    content: '';
    display: inline-block;
    width: .34em;
    height: .34em;
    margin-right: .32em;
    vertical-align: .08em;
    border-radius: 50%;
    border: 1.5px solid currentColor;
    opacity: .55;
}
.voice-swatch.voice-supertonic:hover { border-color: #58a6ff; color: #58a6ff; }
.voice-swatch.voice-supertonic.active { background: #58a6ff; border-color: #58a6ff; color: #06121f; }
