/* ============================================================
   Openings Game — design tokens
   ============================================================ */
:root {
  --navy-950: #0a1730;
  --navy-900: #10203f;
  --navy-800: #1a2f57;
  --navy-600: #33507f;
  --cream: #faf6ec;
  --cream-line: #dfd6bd;
  --paper: #fffdf6;
  --pencil: #e2914c;
  --pencil-dark: #c9762f;
  --ink-green: #4c8c6b;
  --ink-red: #b1473f;
  --ink-orange: #8a5a17;
  --violet: #8f6fae;
  --violet-dark: #6f4f8e;
  --current-blue: #3f7dc9;
  --text-body: #2a2f3d;
  --text-muted: #6b7280;

  --font-display: "Plus Jakarta Sans", "Inter", "Segoe UI", sans-serif;
  --font-body: "Inter", "Segoe UI", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;

  /* Radius scale — one consistent set of stops instead of ad-hoc
     6/8/10/14px values scattered through the file below. Everything
     that used to read var(--radius) still does; components that had a
     literal px value keep their own literal but drawn from this same
     scale, so cards/buttons/popovers/inputs all feel like one system. */
  --radius-sm: 10px;
  --radius: 14px;
  --radius-lg: 20px;
  --radius-xl: 28px;

  /* Softer, more diffuse shadows (modern "ambient" look) plus a
     slightly stronger one for hover/lift states, and a shared
     easing curve for hover/press transitions used throughout. */
  --shadow-card: 0 1px 2px rgba(10, 23, 48, 0.04), 0 12px 32px -8px rgba(10, 23, 48, 0.14);
  --shadow-lift: 0 4px 10px rgba(10, 23, 48, 0.06), 0 20px 44px -10px rgba(10, 23, 48, 0.22);
  --shadow-pop: 0 16px 40px rgba(10, 23, 48, 0.18);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* Set on <html> at runtime by js/free-banner.js to the rendered
     height of the free-limited-time banner (0px when it's hidden, the
     default here). Only actually read by body.trainer-page .container
     below — every other page just lets the banner add its own height
     to normal document flow and doesn't need this at all. */
  --banner-height: 0px;

  /* Board theme — overridden at runtime by js/board-themes.js.
     Default is the classic brown/peach look. */
  --board-light: #f0d9b5;
  --board-dark: #b58863;
  /* Last-move tint (see .board-square.last-move below) — also set at
     runtime by js/board-themes.js, alongside --board-light/-dark. This
     default (a soft ink-green, same rgb as --ink-green) reads fine on
     every board color EXCEPT the "Forest Green" theme, where the board's
     own dark squares (#86a666) are close enough to this same green that
     the highlight nearly disappears. board-themes.js swaps this one
     variable to a warm amber specifically when that theme is active;
     every other theme keeps this default. */
  --last-move-tint: rgba(76, 140, 107, 0.38);

  /* Status colors for form messages / chat feedback bubbles. Split out
     from the one-off hex values they used to be so dark mode (below)
     can repaint them without hunting through the file. */
  --error-bg: #fbeceb;
  --error-text-strong: #6e2a24;
  --success-bg: #ecf5f0;
  --success-text-strong: #204a35;
  --warning-bg: #fbedd3;
}

/* ============================================================
   Dark mode — toggled by js/theme.js setting
   <html data-theme="dark">. Every color below is a straight
   repaint of the same tokens used everywhere above; no selector
   in the rest of this file needs to know which mode is active.

   The mapping keeps each token's ROLE the same across modes:
   --cream is always "the page background", --navy-900 is always
   "the strong ink color used for headings/buttons/borders" — so in
   dark mode the ones that were light become dark and the ones that
   were dark (used as ink/foreground) become light, rather than
   introducing a second, parallel set of dark-only class names.
   Accent colors (pencil orange, violet, ink-green/red, per-course
   accents) are left as-is: they're already saturated enough to read
   fine on a dark background, and keeping them constant means a
   screenshot/course-color someone remembers looks the same in both
   modes. Board square colors are also left alone on purpose — the
   board has its own separate color picker (js/board-themes.js);
   dark mode is about the site chrome around it, not the board itself. */
[data-theme="dark"] {
  --navy-950: #f5f7fb;
  --navy-900: #eef1f8;
  --navy-800: #d7deec;
  --navy-600: #93aad6;
  --cream: #0b1424;
  --cream-line: #2a3a58;
  --paper: #162238;
  --text-body: #e6e9f1;
  --text-muted: #8f9ab0;

  --error-bg: #3a1f1e;
  --error-text-strong: #f0b3ac;
  --success-bg: #17301f;
  --success-text-strong: #a9e0c0;
  --warning-bg: #3a2c14;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font-body);
  color: var(--text-body);
  background: var(--cream);
  /* Flat, modern background — the old ruled-notebook lines (a
     repeating 1px linear-gradient) are gone. A very faint radial
     wash keeps the page from feeling completely flat without
     reading as "paper." Same --cream base color either way, so
     nothing about the brand's palette changes. */
  background-image:
    radial-gradient(1200px 640px at 8% -10%, rgba(63, 125, 201, 0.05), transparent 60%),
    radial-gradient(900px 520px at 100% 0%, rgba(226, 145, 76, 0.06), transparent 55%);
  background-repeat: no-repeat;
  background-attachment: fixed;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* Landing / auth pages sit on plain cream, no wash, less busy */
body.plain-bg { background-image: none; }

h1, h2, h3 { font-family: var(--font-display); color: var(--navy-900); margin: 0 0 0.4em; letter-spacing: -0.015em; font-weight: 700; }
h1 { font-size: clamp(2.4rem, 5vw, 3.6rem); font-weight: 700; }
h2 { font-size: 1.9rem; font-weight: 700; }
h3 { font-size: 1.25rem; font-weight: 700; }
p { margin: 0 0 1em; }
a { color: inherit; transition: color 0.15s var(--ease); }

.container { max-width: 1080px; margin: 0 auto; padding: 0 24px; }
.container-narrow { max-width: 480px; margin: 0 auto; padding: 0 24px; }

/* ---------------- Buttons ---------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-body); font-weight: 600; font-size: 0.95rem;
  padding: 13px 24px; border-radius: 999px; border: 2px solid transparent;
  cursor: pointer; text-decoration: none;
  transition: transform 0.18s var(--ease), box-shadow 0.18s var(--ease), background 0.18s var(--ease), border-color 0.18s var(--ease), color 0.18s var(--ease);
}
.btn:active { transform: scale(0.97); }
.btn-primary { background: var(--navy-900); color: var(--cream); box-shadow: 0 8px 20px -6px rgba(10, 23, 48, 0.45); }
.btn-primary:hover { background: var(--navy-800); transform: translateY(-2px); box-shadow: 0 14px 28px -8px rgba(10, 23, 48, 0.5); }
.btn-outline { background: transparent; border-color: var(--navy-900); color: var(--navy-900); }
.btn-outline:hover { background: var(--navy-900); color: var(--cream); transform: translateY(-2px); }
.btn-block { width: 100%; }
.btn-ghost { background: transparent; color: var(--navy-800); border: none; text-decoration: underline; padding: 4px 0; }

/* ---------------- Free-for-a-limited-time banner ----------------
   Injected by js/free-banner.js as document.body's very first child
   (above .container entirely, not inside .site-header) whenever
   site_settings.free_banner_enabled is true. Plain, full-width,
   in normal document flow on every page except trainer pages, where
   --banner-height (set by that same script) is subtracted back out of
   .container's viewport-height calc above so the board layout still
   fills exactly what's left of the screen instead of overflowing it. */
.free-banner {
  /* --pencil is one of the constant accent colors (deliberately not
     remapped by dark mode, see the comment on [data-theme="dark"]
     above), so its text color is fixed too rather than using
     --navy-950 — that variable flips to a LIGHT color in dark mode
     (it means "ink" there), which would be unreadable on this
     always-orange background. */
  background: var(--pencil); color: #1a1206;
  text-align: center; font-weight: 600;
  /* Fluid font size: scales continuously with viewport width (2.35vw)
     between a 0.45rem floor and a 0.88rem ceiling, instead of jumping
     between fixed sizes at breakpoints. The 2.35vw factor was reverse-
     engineered by rendering the actual banner sentence at a matrix of
     real widths (430/414/390/375/360/340/320/300/280) and font sizes
     to find, at each width, the largest size that still fit on one
     line — then fit a line through those points and added a ~15%
     safety margin (the test used a generic sans-serif; the site's
     actual font, Source Sans 3, may measure a little differently) so
     production doesn't run tighter than what was actually verified.
     white-space: nowrap forbids wrapping outright (belt) and
     text-overflow: ellipsis (suspenders) mean that on the very
     narrowest real devices — where even the 0.45rem floor is a hair
     too wide for the full sentence — the end truncates with "…"
     instead of ever breaking onto a second line, so "always stay in
     one line" holds even in that edge case. Padding is a fixed 8px
     (not scaled) to match exactly what that test matrix measured, so
     the vw formula's fit guarantee isn't undercut by extra padding
     eating into the width it accounted for. */
  font-size: clamp(0.45rem, 2.35vw, 0.88rem);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding: 9px 8px;
  line-height: 1.4;
}

/* ---------------- Header / Nav ---------------- */
.site-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 22px 0; position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--cream) 82%, transparent);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--cream-line);
  transition: border-color 0.2s var(--ease);
}
/* Trainer pages already manage their own fixed-viewport layout
   (body.trainer-page .container above) — sticky positioning here is
   harmless there too since that flex column already treats the
   header as a fixed-size, non-scrolling first child. */
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; }
.brand img { height: 50px; display: block; }
/* Two logo files ship (assets/logo.png / assets/logo-dark.png); which
   one shows is pure CSS keyed off the same [data-theme] attribute the
   rest of dark mode uses, so it switches in the same synchronous paint
   as everything else — no JS, no flash of the wrong logo on load. */
.brand .brand-logo-dark { display: none; }
[data-theme="dark"] .brand .brand-logo-light { display: none; }
[data-theme="dark"] .brand .brand-logo-dark { display: block; }
.nav-actions { display: flex; align-items: center; gap: 22px; font-weight: 600; }
.nav-user { color: var(--navy-800); font-family: var(--font-mono); font-size: 0.85rem; }
.nav-link { text-decoration: none; color: var(--navy-800); }

/* Hamburger button + the panel it opens. Hidden above the mobile
   breakpoint (see the media query below), where .nav-collapsible's
   contents just sit inline in the header as normal. */
.mobile-menu-toggle {
  display: none; flex: none; align-items: center; justify-content: center;
  width: 42px; height: 42px; border-radius: var(--radius-sm); border: 1.5px solid var(--cream-line);
  background: var(--paper); color: var(--text-body); cursor: pointer;
  box-shadow: var(--shadow-card);
  transition: border-color 0.15s var(--ease), color 0.15s var(--ease), transform 0.15s var(--ease);
}
.mobile-menu-toggle:hover { transform: translateY(-1px); }
.mobile-menu-toggle svg { width: 19px; height: 19px; }
.mobile-menu-toggle:hover, .mobile-menu-toggle[aria-expanded="true"] { border-color: var(--pencil); color: var(--pencil-dark); }
.nav-collapsible { display: flex; align-items: center; gap: 22px; }
/* Same [hidden]-vs-bare-display gotcha documented above
   (.course-full-list[hidden]/.filter-panel[hidden]/etc.) — dashboard.html's
   Phase 5 signed-in/signed-out nav swap (js/dashboard.js's
   toggleAuthChrome()) toggles the `hidden` attribute on these, which
   the bare `.nav-collapsible { display: flex }` rule above would
   otherwise silently defeat. */
.nav-collapsible[hidden] { display: none; }

/* Dark/Bright toggle (js/theme.js). Label text is set in JS to whichever
   mode the button switches TO — "Dark" while the site is bright, "Bright"
   while it's dark — so it always reads as an action, not a status readout. */
.theme-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-body); font-weight: 600; font-size: 0.95rem;
  padding: 10px 18px; border-radius: 999px; border: 1.5px solid var(--cream-line);
  background: var(--paper); color: var(--text-body); cursor: pointer;
  box-shadow: var(--shadow-card);
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease), transform 0.15s var(--ease);
}
.theme-toggle:hover { border-color: var(--pencil); transform: translateY(-1px); }
/* Auth pages (login/signup/etc.) don't have a .nav-actions bar — the
   toggle sits at the top-right of the narrow auth shell instead. */
.auth-shell { position: relative; }
.auth-shell .theme-toggle { position: absolute; top: 18px; right: 18px; }

/* Header streak chip — the same "Streak N days" pill that used to live
   under the dashboard heading, just resized to sit in the header next
   to the theme toggle on every logged-in page (courses.html, profile.html,
   train.html, not just dashboard.html). Populated by js/nav.js. */
.header-streak {
  display: flex; align-items: center; gap: 6px; background: var(--navy-900); color: var(--cream);
  padding: 9px 16px; border-radius: 999px; font-family: var(--font-mono); font-size: 0.9rem;
  white-space: nowrap; box-shadow: 0 8px 20px -8px rgba(10, 23, 48, 0.5);
}
.header-streak .streak-num { font-weight: 700; color: var(--pencil); }

/* Header "Go Pro" CTA — see dashboard.html's comment on the element
   itself for why it lives outside .nav-collapsible. Gold/accent
   (--pencil) rather than the navy .btn-primary look, so it reads as a
   distinct upsell rather than just another nav action, and stays
   compact enough to sit comfortably next to the theme toggle. */
.btn-go-pro {
  padding: 9px 16px; font-size: 0.85rem;
  background: var(--pencil); border-color: var(--pencil); color: var(--navy-950);
}
.btn-go-pro:hover { background: var(--pencil-dark); border-color: var(--pencil-dark); }
/* .btn sets display: inline-flex as a same-specificity author rule,
   which beats the browser's own [hidden] { display: none } (author
   rules always win over the user-agent stylesheet, specificity aside
   — see the .admin-menu-link comment elsewhere in this file for the
   same issue). Without this, toggling the `hidden` attribute/property
   on this link from js/nav.js does nothing visually — it stayed
   permanently visible, including for signed-out visitors, which is
   the bug this rule fixes. */
.btn-go-pro[hidden] { display: none; }

/* Trophy icon — header link to leaderboard.html, styled to match the
   profile icon button next to it (see PROGRESS.md "Leaderboards"
   session). js/nav.js rewrites its href on course-scoped pages
   (drill/puzzles/timetrial) to deep-link into that course + mode. */
.trophy-btn {
  display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 50%; padding: 0;
  border: 1.5px solid var(--cream-line); background: var(--paper); color: var(--navy-900);
  text-decoration: none; box-shadow: var(--shadow-card);
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease), transform 0.15s var(--ease);
}
.trophy-btn:hover, .trophy-btn.is-active { border-color: var(--pencil); transform: translateY(-1px); }
.trophy-btn svg { width: 22px; height: 22px; }

/* Profile icon + dropdown menu, replacing the old flat row of text links
   and a separate "Log out" button. */
.profile-menu { position: relative; }
.profile-menu-btn {
  display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 50%; padding: 0;
  border: 1.5px solid var(--cream-line); background: var(--paper); color: var(--navy-900);
  cursor: pointer; box-shadow: var(--shadow-card);
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease), transform 0.15s var(--ease);
}
.profile-menu-btn:hover { border-color: var(--pencil); transform: translateY(-1px); }
.profile-menu-btn svg { width: 22px; height: 22px; }
.profile-menu-dropdown {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 30;
  min-width: 200px; background: var(--paper); border: 1px solid var(--cream-line);
  border-radius: var(--radius); box-shadow: var(--shadow-pop);
  padding: 8px; display: flex; flex-direction: column; gap: 2px;
}
.profile-menu-dropdown[hidden] { display: none; }
.profile-menu-dropdown-user {
  font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted);
  padding: 8px 12px 10px; border-bottom: 1px solid var(--cream-line); margin-bottom: 4px;
  overflow: hidden; text-overflow: ellipsis;
}
/* :not([hidden]) matters here, not just style: this is an author-
   stylesheet rule with no !important, and author rules always beat
   the browser's default `[hidden] { display: none }` (which lives in
   the user-agent stylesheet) regardless of selector specificity. Left
   as plain `.profile-menu-dropdown a`, this line alone forces
   display:block on the admin-only link for every visitor — the
   `:not([hidden])` on .admin-menu-link further down in this file only
   patches its own flex layout, it doesn't stop this rule from already
   having made the link visible. */
.profile-menu-dropdown a:not([hidden]), .profile-menu-dropdown button {
  display: block; width: 100%; text-align: left; text-decoration: none;
  font-family: var(--font-body); font-weight: 600; font-size: 0.92rem; color: var(--navy-900);
  background: transparent; border: none; border-radius: 12px; padding: 9px 12px; cursor: pointer;
}
.profile-menu-dropdown a:hover, .profile-menu-dropdown button:hover { background: var(--cream-line); }
.profile-menu-dropdown-signout { color: var(--ink-red); }

/* Below 640px the header can't fit the logo, Dark/Bright toggle, and
   everything else (streak chip + profile icon on the logged-in pages;
   Log in / Start training on the landing page) without overflowing or
   wrapping awkwardly. Everything after the theme toggle collapses into
   .nav-collapsible, which becomes a dropdown panel opened by the new
   hamburger button (js/mobile-menu.js) — so the header row only ever
   has to fit logo — Dark/Bright — hamburger, with Dark/Bright sitting
   right where it already was, between the logo and the new button. */
@media (max-width: 640px) {
  .mobile-menu-toggle { display: flex; }
  .nav-collapsible {
    display: none; position: absolute; top: calc(100% + 8px); left: 24px; right: 24px;
    flex-direction: column; align-items: stretch; gap: 14px;
    background: var(--paper); border: 1px solid var(--cream-line); border-radius: var(--radius-lg);
    box-shadow: var(--shadow-pop); padding: 18px 20px; z-index: 40;
  }
  .nav-collapsible.is-open { display: flex; }
  .nav-collapsible .header-streak { justify-content: center; }
  .nav-collapsible .trophy-btn { align-self: center; }
  .nav-collapsible .nav-link { text-align: center; padding: 6px 0; }
  .nav-collapsible .btn { width: 100%; }
  /* Inside the panel, show the profile links flattened and already
     visible instead of nested behind a second icon tap — opening the
     hamburger is already the "opt in" gesture, a second tap-to-reveal
     on top of that would just be an extra step for no reason. The
     !important is scoped tightly to this one nested context; it's
     needed because .profile-menu-dropdown[hidden] normally means
     display:none, and that state is what ships by default (JS only
     ever removes "hidden" in response to tapping the icon button this
     rule hides), so without it the flattened list wouldn't render. */
  .nav-collapsible .profile-menu-btn { display: none; }
  .nav-collapsible .profile-menu-dropdown {
    display: flex !important; position: static; box-shadow: none; border: none;
    padding: 0; min-width: 0; width: 100%;
  }
}

/* ---------------- Landing hero ---------------- */
.hero {
  padding: 64px 0 40px;
  display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 48px; align-items: center;
}
/* Compact variant for dashboard.html's signed-out banner. The base
   .hero above (still used as-is by submit-course.html) is a tall,
   two-column layout with a big heading and a notebook illustration —
   fine as a one-off page intro, but too much vertical space to sit
   above a course grid people came to actually use. This drops the
   grid down to one column, cuts the padding roughly in half, and
   shrinks the heading so the whole banner reads as a strip, not a
   dedicated landing section. */
.hero-compact {
  display: block;
  padding: 28px 0 20px;
}
.hero-compact .hero-eyebrow { margin-bottom: 8px; }
.hero-compact h1 { font-size: clamp(1.6rem, 3.2vw, 2.1rem); margin-bottom: 8px; }
.hero-compact .hero-sub { font-size: 1rem; max-width: 60ch; }
.hero-compact .hero-actions { margin-top: 16px; }
/* Same [hidden]-vs-bare-display gotcha as .nav-collapsible[hidden]
   above — dashboard.html's signed-out hero banner (data-signed-out-hero)
   is toggled via the `hidden` attribute by js/dashboard.js. */
.hero[hidden] { display: none; }
.hero-eyebrow {
  font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.14em;
  font-size: 0.78rem; color: var(--pencil-dark); font-weight: 700; margin-bottom: 14px;
  display: inline-block; background: var(--paper); border: 1px solid var(--cream-line);
  padding: 6px 14px; border-radius: 999px; box-shadow: var(--shadow-card);
}
.hero-sub { font-size: 1.15rem; color: var(--text-muted); max-width: 42ch; }
.hero-actions { display: flex; gap: 14px; margin-top: 28px; }

.notebook-mock {
  background: var(--paper); border-radius: var(--radius-lg); box-shadow: var(--shadow-lift);
  padding: 28px 30px; position: relative; border-top: none;
  font-family: var(--font-mono); font-size: 1.05rem; color: var(--navy-900);
  transition: transform 0.3s var(--ease);
}
.notebook-mock:hover { transform: translateY(-4px); }
.notebook-mock .mock-line { padding: 7px 0; border-bottom: 1px solid var(--cream-line); }
.notebook-mock .mock-line:last-child { border-bottom: none; }
.notebook-mock .mock-caption { font-family: var(--font-body); color: var(--text-muted); font-size: 0.85rem; margin-top: 14px; }

.features {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; padding: 40px 0 80px;
}
.feature-card {
  background: var(--paper); border-radius: var(--radius-lg); padding: 28px; box-shadow: var(--shadow-card);
  border: 1px solid var(--cream-line);
  transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.feature-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-lift); }
.feature-card .feature-mark {
  font-family: var(--font-mono); color: var(--pencil-dark); font-weight: 700; font-size: 0.8rem;
  text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 10px; display: block;
}

/* ---------------- Auth forms ---------------- */
.auth-shell {
  min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 40px 0;
}
.auth-card {
  background: var(--paper); border-radius: var(--radius-xl); box-shadow: var(--shadow-lift);
  padding: 44px 38px; width: 100%; border-top: none;
  border: 1px solid var(--cream-line);
  position: relative; overflow: hidden;
}
/* Thin accent bar replaces the old heavy 6px solid border-top — same
   idea (a hairline of brand color at the top of the card) with a
   softer, more modern weight. */
.auth-card::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px;
  background: linear-gradient(90deg, var(--pencil), var(--violet));
}
.auth-card .brand { justify-content: center; margin-bottom: 22px; }
.auth-card h1 { font-size: 1.7rem; text-align: center; }
.auth-sub { text-align: center; color: var(--text-muted); margin-bottom: 26px; }

.field { margin-bottom: 18px; }
.field label { display: block; font-weight: 600; font-size: 0.88rem; margin-bottom: 6px; color: var(--navy-900); }
.field input, .field select, .field textarea {
  width: 100%; padding: 13px 15px; border-radius: var(--radius-sm); border: 1.5px solid var(--cream-line);
  font-family: var(--font-body); font-size: 1rem; background: var(--paper);
  color: var(--text-body); caret-color: var(--pencil);
  transition: border-color 0.15s var(--ease), box-shadow 0.15s var(--ease);
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--pencil);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--pencil) 18%, transparent);
}
.field small { display: block; margin-top: 6px; color: var(--text-muted); font-size: 0.82rem; }

/* Password fields with a show/hide eye button — wraps the existing
   .field input markup rather than replacing it, so nothing else about
   .field styling (focus ring, spacing) needs to change. */
.field-password { position: relative; }
.field-password input { padding-right: 44px; } /* room for the button so text never runs under it */
.password-toggle-btn {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: 10px; cursor: pointer;
  color: var(--text-muted); padding: 0;
}
.password-toggle-btn:hover { color: var(--text-body); background: var(--cream-line); }
.password-toggle-btn:focus-visible { outline: 2px solid var(--pencil); outline-offset: 1px; }
.password-toggle-btn svg { width: 20px; height: 20px; }
/* The "eye" (show) icon is the default/hidden state; swapping to
   .is-visible on click shows the "eye-off" (currently visible) icon
   instead — same two-icon pattern as the rest of the app's toggle
   buttons, just done in CSS rather than swapping innerHTML. */
.password-toggle-btn .icon-eye-off { display: none; }
.password-toggle-btn.is-visible .icon-eye { display: none; }
.password-toggle-btn.is-visible .icon-eye-off { display: block; }
/* Pasted move lists read far better in a monospace font (lines up move
   numbers/pieces the way a PGN naturally does), and this needs to be
   tall enough to paste a real opening tree into without immediately
   scrolling — a single-line-input's height would make that miserable. */
.field textarea {
  font-family: var(--font-mono); font-size: 0.92rem; line-height: 1.6;
  min-height: 220px; resize: vertical;
}

/* Repeatable per-variation blocks on submit-course.html — one card per
   variation (moves textarea + its own optional comment box), instead
   of the old single big textarea for every line. Starts with one
   block; "+ Add another variation" appends more, endlessly. */
.variations-list { display: flex; flex-direction: column; gap: 14px; margin-bottom: 12px; }
.variation-block {
  border: 1.5px solid var(--cream-line); border-radius: 14px; background: var(--paper);
  padding: 14px; position: relative;
}
.variation-block-header {
  display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;
}
.variation-block-label { font-weight: 600; font-size: 0.82rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
.variation-remove-btn {
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 0.82rem; padding: 2px 6px; border-radius: 10px; text-decoration: underline;
}
.variation-remove-btn:hover { color: var(--ink-red); background: var(--cream-line); }
.variation-block .variation-moves-input {
  width: 100%; padding: 10px 12px; border-radius: 12px; border: 1.5px solid var(--cream-line);
  font-family: var(--font-mono); font-size: 0.92rem; line-height: 1.5;
  min-height: 70px; resize: vertical; background: var(--cream); color: var(--text-body);
  caret-color: var(--pencil); margin-bottom: 10px;
}
.variation-block .variation-moves-input:focus,
.variation-block .variation-comment-input:focus { outline: 2px solid var(--pencil); outline-offset: 1px; border-color: var(--pencil); }
.variation-block .variation-comment-input {
  width: 100%; padding: 9px 12px; border-radius: 12px; border: 1.5px solid var(--cream-line);
  font-family: var(--font-body); font-size: 0.9rem; background: var(--cream);
  color: var(--text-body); caret-color: var(--pencil); min-height: 50px; resize: vertical;
}
.btn-add-variation { margin-bottom: 6px; }

/* "Your submissions" list on submit-course.html — deliberately plain
   (reuses .profile-card as the outer shell) since this is a status
   list, not something that needs its own visual language. */
.submission-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid var(--cream-line);
}
.submission-row:last-child { border-bottom: none; }
.submission-row-title { font-weight: 600; }
.submission-row-meta { font-size: 0.82rem; color: var(--text-muted); margin-top: 2px; }
.submission-status {
  font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em;
  padding: 4px 10px; border-radius: 999px; white-space: nowrap;
}
.submission-status-pending { background: var(--warning-bg); color: var(--ink-orange); }
.submission-status-approved { background: var(--success-bg); color: var(--ink-green); }
.submission-status-rejected { background: var(--error-bg); color: var(--ink-red); }

.form-message { margin-top: 14px; font-size: 0.9rem; padding: 10px 12px; border-radius: 12px; display: none; }
.form-message.visible { display: block; }
.form-message.error { background: var(--error-bg); color: var(--ink-red); }
.form-message.success { background: var(--success-bg); color: var(--ink-green); }
/* Same visual as .form-message.error, but with no `display` rule of its
   own — see reset-password.html for why: it needs the `hidden`
   attribute alone to control visibility without an author CSS rule
   fighting it. */
.auth-error-block { margin-top: 14px; font-size: 0.9rem; padding: 10px 12px; border-radius: 12px; background: var(--error-bg); color: var(--ink-red); }
.auth-error-block a { color: inherit; text-decoration: underline; }
/* Neutral variant of .auth-error-block — same box, calmer colors, for
   informational states (e.g. signup.html's "check your inbox") rather
   than an actual error. Combined as class="auth-error-block auth-info-block". */
.auth-info-block { background: var(--paper); color: var(--text-body); border: 1.5px solid var(--cream-line); }
.auth-info-block p { margin: 0 0 10px; }
/* Third variant, same pattern — green success colors. Combined as
   class="auth-error-block auth-success-block". Added for
   choose-plan.html / checkout-result.js (billing rollout Phase 4). */
.auth-success-block { background: var(--success-bg); color: var(--ink-green); }

/* ---------------- Plan picker (choose-plan.html) ----------------
   Billing rollout Phase 4. Reuses .auth-shell/.auth-card's centered-
   card visual language (same pencil-orange top border, same paper
   background) but choose-plan.html wraps it in .container instead of
   .container-narrow, since two plan cards side by side need more
   width than the single-column login/signup forms ever did. */
.plan-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 24px 0; }
@media (max-width: 640px) { .plan-grid { grid-template-columns: 1fr; } }
.plan-card {
  border: 1px solid var(--cream-line); border-radius: var(--radius-lg);
  padding: 26px; display: flex; flex-direction: column; gap: 14px;
  background: var(--paper); box-shadow: var(--shadow-card);
  transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.plan-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lift); }
.plan-card h3 { margin: 0; font-family: var(--font-display); font-size: 1.25rem; }
.plan-card-price { font-family: var(--font-display); font-size: 1.9rem; color: var(--navy-900); }
.plan-card-price span { font-size: 0.95rem; font-weight: 400; color: var(--text-muted); }
.plan-card-actions { display: flex; flex-direction: column; gap: 10px; margin-top: auto; }
.plan-card-note { font-size: 0.82rem; color: var(--text-muted); margin: 0; }
/* Shown instead of the trial button once a fingerprint's been
   blocked from trials (subscription.status === 'blocked_trial') —
   direct-pay is still offered at any classification per locked
   decisions, only the trial button itself is withheld. */
.plan-card-trial-blocked { font-size: 0.82rem; color: var(--text-muted); font-style: italic; }

.btn-link {
  background: none; border: none; padding: 0; margin: 0; cursor: pointer;
  font-family: var(--font-body); font-size: 0.88rem; font-weight: 600; color: var(--pencil-dark); text-decoration: underline;
}
.btn-link:disabled { color: var(--text-muted); text-decoration: none; cursor: default; }

.auth-switch { text-align: center; margin-top: 20px; color: var(--text-muted); font-size: 0.92rem; }

/* ---------------- Dashboard ---------------- */
.dashboard-top { padding: 34px 0 10px; display: flex; justify-content: space-between; align-items: flex-end; flex-wrap: wrap; gap: 16px; }
/* This was an <h1> before Phase 5 (billing rollout) added the
   signed-out hero banner above it, which has its own, real <h1> --
   demoted to <h2> for one-h1-per-page semantics/accessibility, with
   its old visual size kept here so logged-in users (who never see the
   hero banner) see no visual change at all. */
.dashboard-top h2 { font-size: clamp(2.4rem, 5vw, 3.6rem); font-weight: 600; }
/* Used on dashboard.html only, where the hero heading + subtitle now
   stand alone (no streak chip beside them anymore — that moved into
   the header). courses.html keeps the plain .dashboard-top, left-aligned. */
.dashboard-top-centered { justify-content: center; text-align: center; padding-bottom: 34px; }
/* The heading+subtitle wrapper is the sole flex item here. Flex items
   default to min-width:auto, which stops them shrinking below their
   content's natural width — on very narrow phones that pushed this
   div wider than the viewport (overflowing right, which then reads as
   the whole centered block having shifted left, since the page can
   only scroll from the left edge). min-width:0 lets it shrink to the
   container width like a normal block so the text wraps instead of
   overflowing, and width:100% keeps it centered/full-width so
   text-align:center has the whole row to center within. */
.dashboard-top-centered > div { min-width: 0; width: 100%; }
.dashboard-top-centered .hero-sub { margin: 0 auto; max-width: none; white-space: nowrap; }
.streak-chip {
  display: flex; align-items: center; gap: 10px; background: var(--navy-900); color: var(--cream);
  padding: 10px 18px; border-radius: 999px; font-family: var(--font-mono);
  box-shadow: 0 10px 24px -10px rgba(10, 23, 48, 0.55);
}
.streak-chip .streak-num { font-size: 1.3rem; font-weight: 700; color: var(--pencil); }
/* ---------------- Repertoire tabs (this session) ---------------- */
/* Left = Your Repertoire, right = Openings, sitting above the normal
   search/filter toolbar — these switch which SET of courses is being
   browsed (the user's own picks vs. the full library), not a filter
   within one set, so they get their own row/component instead of
   living inside .filter-panel with the side/progress checkboxes. */
.repertoire-tabs {
  display: flex; gap: 6px; margin: 18px 0 10px; padding: 4px;
  background: var(--cream-line); border-radius: 999px; width: fit-content;
}
.repertoire-tab {
  border: none; background: transparent; cursor: pointer; padding: 8px 18px;
  border-radius: 999px; font-family: var(--font-mono); font-size: 0.82rem; font-weight: 600;
  color: var(--text-muted); transition: background 0.15s var(--ease), color 0.15s var(--ease);
}
.repertoire-tab:hover { color: var(--text-body); }
.repertoire-tab.active { background: var(--paper); color: var(--navy-900); box-shadow: var(--shadow-card); font-weight: 700; }
@media (max-width: 560px) {
  .repertoire-tabs { width: 100%; }
  .repertoire-tab { flex: 1 1 0; text-align: center; padding: 8px 10px; }
}

/* ---------------- Opening list (home page) ---------------- */
.opening-list { display: flex; flex-direction: column; gap: 14px; padding: 18px 0 20px; }
.opening-list-loading {
  font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted);
  text-align: center; padding: 10px 0 40px; margin: 0;
}
.opening-card {
  display: grid; grid-template-columns: 168px 1fr; align-items: stretch; gap: 20px;
  background: var(--paper); border-radius: var(--radius-lg); padding: 18px; text-decoration: none;
  color: var(--text-body); box-shadow: var(--shadow-card); border-left: 6px solid var(--accent, var(--pencil));
  border-top: 1px solid var(--cream-line); border-right: 1px solid var(--cream-line); border-bottom: 1px solid var(--cream-line);
  transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.opening-card:hover { transform: translateY(-3px) translateX(2px); box-shadow: var(--shadow-lift); }
.opening-card:hover .opening-card-title { color: var(--accent, var(--pencil-dark)); }
.opening-card-board { align-self: center; width: 100%; }
.board-grid-mini { border-width: 3px; border-radius: var(--radius-sm); box-shadow: none; }
.board-grid-mini .board-square { cursor: default; }
.opening-card-main { min-width: 0; display: flex; flex-direction: column; }
.opening-card-title { margin: 0; font-size: 1.15rem; transition: color 0.12s var(--ease); }
.opening-card-desc {
  color: var(--text-muted); font-size: 0.9rem; margin: 6px 0 0; line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.opening-card-meta {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin-top: auto; padding-top: 14px; font-family: var(--font-mono); font-size: 0.76rem;
}
.opening-card-tier { color: var(--text-body); font-weight: 600; }
.opening-card-total { color: var(--text-muted); white-space: nowrap; }
.opening-card-progress { display: flex; gap: 4px; margin-top: 6px; }
.tier-segment {
  flex-grow: var(--tier-share, 1); flex-shrink: 1; flex-basis: 0;
  position: relative; height: 8px; border-radius: 999px; background: var(--cream-line); overflow: hidden;
}
.tier-fill-learned, .tier-fill-mastered { position: absolute; inset: 0; height: 100%; border-radius: 999px; }
.tier-fill-learned { background: var(--pencil); }
.tier-fill-mastered { background: var(--violet); }
.opening-card-cta {
  font-family: var(--font-mono); font-size: 0.82rem; font-weight: 600; color: var(--navy-900);
  margin-top: 10px;
}
.opening-list-empty {
  font-family: var(--font-mono); font-size: 0.85rem; color: var(--text-muted); text-align: center; padding: 40px 0;
}

/* ---------------- "See full list" per-course line list (this session) ---------------- */
/* .opening-card-wrap holds the normal .opening-card <a> plus the new
   toggle + collapsible list beneath it. The toggle/list are siblings of
   the card, not nested inside it, for the same reason .pgn-match-group
   keeps its own sub-list as a sibling — the card is itself an <a>, and
   an interactive element can't safely nest another one inside it. */
.opening-card-wrap { display: flex; flex-direction: column; position: relative; }

/* Small bookmark-style add/remove button, overlaid on the card's own
   top-right corner — a sibling of the card <a>, not nested inside it
   (see buildRepertoireToggle's comment in js/opening-list.js for why).
   Unfilled outline = not in the repertoire yet; filled = already in
   it, same visual language as a browser bookmark icon. */
.repertoire-toggle-btn {
  position: absolute; top: 12px; right: 12px; z-index: 2;
  width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
  border: none; border-radius: 12px; background: var(--paper); box-shadow: var(--shadow-card);
  color: var(--text-muted); cursor: pointer; transition: color 0.15s var(--ease), transform 0.1s var(--ease);
}
.repertoire-toggle-btn svg { width: 16px; height: 16px; }
.repertoire-toggle-btn:hover { color: var(--pencil-dark); transform: scale(1.08); }
.repertoire-toggle-btn.in-repertoire { color: var(--pencil); }
.repertoire-toggle-btn:disabled { opacity: 0.6; cursor: default; transform: none; }
.course-full-list-toggle {
  align-self: flex-start; margin: 8px 0 0 4px; padding: 6px 12px;
  border-radius: 999px; border: 1.5px solid var(--cream-line); background: var(--paper);
  color: var(--text-body); font-family: var(--font-mono); font-size: 0.76rem; font-weight: 600;
  cursor: pointer; transition: border-color 0.15s var(--ease), color 0.15s var(--ease);
}
.course-full-list-toggle:hover { border-color: var(--accent, var(--pencil)); color: var(--accent, var(--pencil-dark)); }
.course-full-list {
  display: flex; flex-direction: column; gap: 6px;
  margin: 10px 0 0 24px; padding-left: 14px; border-left: 2px solid var(--cream-line);
  /* Can legitimately be 250+ items for a big course (the Sicilian) —
     scroll internally rather than letting one expanded card push every
     card below it off-screen. */
  max-height: 420px; overflow-y: auto;
}
/* Bug fix: the bare `display: flex` above is an AUTHOR rule, and
   author rules beat the browser's default `[hidden] { display: none }`
   (a USER-AGENT rule) regardless of specificity — origin outranks
   specificity in the cascade. Net effect: setting `.hidden = true` in
   JS (js/opening-list.js's toggle handler) added the attribute but the
   element never actually disappeared, which is exactly the reported
   "See full list works, Hide full list does nothing" bug. Same fix
   pattern already used elsewhere in this file for the identical
   problem — see `.profile-menu-dropdown[hidden]`/`.filter-panel[hidden]`/
   `.pgn-panel[hidden]` above — just missing here until now. */
.course-full-list[hidden] { display: none; }
.course-full-list-line {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 8px 12px; background: var(--cream); border-radius: 12px; text-decoration: none;
  color: var(--text-body); transition: background 0.12s var(--ease), color 0.12s var(--ease);
}
.course-full-list-line:hover { background: var(--paper); color: var(--pencil-dark); }
.course-full-list-line-num {
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-muted);
  flex: 0 0 auto; width: 26px; text-align: right;
}
.course-full-list-line-name { font-size: 0.86rem; font-weight: 600; flex: 1 1 auto; }
.course-full-list-line-badge { font-size: 0.9rem; line-height: 1; flex: 0 0 auto; }
.course-full-list-loading, .course-full-list-empty {
  font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted); padding: 8px 12px; margin: 0;
}
.course-full-list-locked-note {
  font-size: 0.78rem; color: var(--text-muted); padding: 8px 12px 4px; margin: 0;
  border-top: 1px dashed var(--pencil);
}
.course-full-list-locked-note a { color: var(--navy-900); font-weight: 600; }

@media (max-width: 640px) {
  .course-full-list { margin-left: 12px; padding-left: 10px; max-height: 320px; }
}

/* ---------------- Mode popover (train.html/drill.html "Mode" toolbar button) ---------------- */
/* Renders into the same .settings-popover element board-color/piece-set
   settings use (js/mode-picker.js's wireModeAndSettingsPopovers swaps
   the popover's content depending which toolbar button was clicked) —
   so this only needs to style what's INSIDE that popover, not the
   popover shell itself (border/background/radius already come from
   .settings-popover). Replaces the old modes.html full-page tile grid
   this app briefly had; that page is gone (see PROGRESS.md) in favor
   of switching modes from right where you're already training. */
.mode-popover-list { display: flex; flex-direction: column; gap: 6px; }
.mode-popover-item {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px;
  border-radius: 14px; text-decoration: none; color: var(--text-body);
  border: 1.5px solid transparent; transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
a.mode-popover-item:hover { border-color: var(--pencil); background: var(--cream); }
.mode-popover-icon { font-size: 1.1rem; line-height: 1; width: 22px; text-align: center; }
.mode-popover-title { font-weight: 600; font-size: 0.9rem; flex: 1 1 auto; }
.mode-popover-meta { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-muted); white-space: nowrap; }
.mode-popover-item-active { border-color: var(--navy-900); }
.mode-popover-item-active .mode-popover-meta { color: var(--navy-900); font-weight: 600; }
.mode-popover-item-disabled { opacity: 0.55; cursor: not-allowed; }

/* Task 5: Hustle's tile is a real <button>, not an <a> or an inert
   <div> — reset the button defaults .mode-popover-item otherwise
   doesn't need to worry about (a/div don't have them), then dim it the
   same way -disabled does, but with a pointer cursor instead of
   not-allowed: clicking it DOES do something (opens the info panel
   below), it just doesn't start a mode. */
button.mode-popover-item {
  width: 100%; background: none; font: inherit; text-align: left; cursor: pointer;
}
.mode-popover-item-locked { opacity: 0.7; }
.mode-popover-item-locked:hover { opacity: 1; border-color: var(--pencil); background: var(--cream); }

/* .mode-popover-item-paywalled removed — Drill/Puzzles/Time Trial are
   no longer gated behind an account (see js/mode-picker.js), so no tile
   uses this class anymore. Only Hustle still uses -locked, above, for
   its "not a mode yet, but not inert either" dashed/dimmed look. */

/* Hustle's info panel (js/mode-picker.js drawInfo()) — replaces the
   mode list inside the SAME popover element rather than opening a
   second overlay; .settings-popover (the shared shell) already
   supplies the border/background/radius/padding this sits inside. */
.mode-popover-info { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.mode-popover-info-back {
  background: none; border: none; padding: 0; margin-bottom: 4px; cursor: pointer;
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em;
}
.mode-popover-info-back:hover { color: var(--text-body); }
.mode-popover-info-icon { font-size: 1.6rem; line-height: 1; }
.mode-popover-info-title { margin: 0; font-size: 1rem; }
.mode-popover-info-body { margin: 0; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; }

/* ---------------- PGN search results: card + matching variations ---------------- */
/* .pgn-match-group wraps a normal .opening-card with a small indented
   sub-list of that course's specific lines that matched the pasted
   PGN, each linking straight into the trainer at that exact line
   (train.html?course=...&line=...). The card itself is still a plain
   <a>, so this list sits as a sibling underneath rather than nesting
   another link inside it. */
.pgn-match-group { display: flex; flex-direction: column; }
.pgn-match-lines {
  display: flex; flex-direction: column; gap: 6px;
  margin: 8px 0 0 24px; padding-left: 14px; border-left: 2px solid var(--cream-line);
}
.pgn-match-line {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 8px 12px; background: var(--cream); border-radius: 12px; text-decoration: none;
  color: var(--text-body); transition: background 0.12s var(--ease), color 0.12s var(--ease);
}
.pgn-match-line:hover { background: var(--paper); color: var(--pencil-dark); }
.pgn-match-line-name { font-size: 0.86rem; font-weight: 600; }
.pgn-match-line-len {
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-muted); white-space: nowrap;
}

@media (max-width: 640px) {
  .pgn-match-lines { margin-left: 12px; padding-left: 10px; }
  .pgn-match-line { flex-direction: column; align-items: flex-start; gap: 2px; }
}

@media (max-width: 640px) {
  /* On narrow screens the hero title and subtitle no longer force a
     single line — they wrap naturally within the container instead of
     overflowing off the edge of the viewport. */
  h1 { font-size: clamp(1.7rem, 8vw, 2.3rem); }
  .dashboard-top-centered .hero-sub { font-size: 0.88rem; white-space: normal; max-width: 34ch; }
}

/* ---------------- Opening list controls (search / sort / filter) ---------------- */
.opening-controls { position: relative; padding: 4px 0 6px; }
.opening-toolbar { display: flex; align-items: center; gap: 10px; }
.filter-btn {
  flex: none; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center;
  border-radius: 14px; border: 1.5px solid var(--cream-line); background: var(--paper); color: var(--text-body);
  cursor: pointer;
}
.filter-btn svg { width: 18px; height: 18px; }
.filter-btn:hover, .filter-btn[aria-expanded="true"] { border-color: var(--pencil); color: var(--pencil-dark); }
.opening-search { flex: 1; min-width: 0; }
.opening-search input {
  width: 100%; padding: 11px 14px; border-radius: 14px; border: 1.5px solid var(--cream-line);
  background: var(--paper); font-family: var(--font-body); font-size: 0.95rem; color: var(--text-body);
}
.opening-search input:focus { outline: 2px solid var(--pencil); outline-offset: 1px; border-color: var(--pencil); }
.opening-count {
  flex: none; display: flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: 0.82rem;
  color: var(--text-muted); white-space: nowrap; padding: 0 4px;
}
.opening-count-icon { width: 18px; height: 18px; border-radius: 5px; overflow: hidden; }
.filter-panel[hidden] { display: none; }
.filter-panel {
  position: absolute; z-index: 20; top: calc(100% + 6px); left: 0;
  display: flex; flex-direction: column; gap: 16px; min-width: 240px;
  background: var(--paper); border: 1.5px solid var(--cream-line); border-radius: var(--radius);
  box-shadow: var(--shadow-card); padding: 16px 18px;
}
.filter-panel-block { display: flex; flex-direction: column; gap: 8px; }
.filter-panel-label {
  font-family: var(--font-mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-muted);
}
.filter-radio, .filter-check {
  display: flex; align-items: center; gap: 8px; font-family: var(--font-body); font-size: 0.9rem;
  color: var(--text-body); cursor: pointer;
}
.filter-radio input, .filter-check input { accent-color: var(--pencil); width: 15px; height: 15px; cursor: pointer; }

/* ---------------- PGN search (paste a PGN, find the matching opening) ---------------- */
.pgn-btn {
  flex: none; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center;
  border-radius: 14px; border: 1.5px solid var(--cream-line); background: var(--paper); color: var(--text-body);
  cursor: pointer;
}
.pgn-btn svg { width: 18px; height: 18px; }
.pgn-btn:hover, .pgn-btn[aria-expanded="true"] { border-color: var(--pencil); color: var(--pencil-dark); }
.pgn-panel[hidden] { display: none; }
.pgn-panel {
  position: absolute; z-index: 20; top: calc(100% + 6px); right: 0;
  display: flex; flex-direction: column; gap: 10px; width: min(420px, 92vw);
  background: var(--paper); border: 1.5px solid var(--cream-line); border-radius: var(--radius);
  box-shadow: var(--shadow-card); padding: 16px 18px;
}
.pgn-panel-label {
  font-family: var(--font-mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-muted);
}
.pgn-panel textarea {
  width: 100%; min-height: 76px; resize: vertical; padding: 10px 12px; border-radius: 12px;
  border: 1.5px solid var(--cream-line); background: var(--cream); font-family: var(--font-mono);
  font-size: 0.85rem; color: var(--text-body); line-height: 1.5;
}
.pgn-panel textarea:focus { outline: 2px solid var(--pencil); outline-offset: 1px; border-color: var(--pencil); }
.pgn-panel-actions { display: flex; gap: 8px; }
.pgn-panel-actions .btn { flex: none; }
.pgn-status { font-family: var(--font-body); font-size: 0.85rem; color: var(--text-muted); min-height: 1.2em; }
.pgn-status.pgn-status-active { color: var(--pencil-dark); font-weight: 600; }

@media (max-width: 640px) {
  .pgn-panel { left: 0; right: 0; width: auto; }
}

@media (max-width: 640px) {
  .opening-card { grid-template-columns: 1fr; }
  .opening-card-board { max-width: 220px; margin: 0 auto; }
  .filter-panel { left: 0; right: 0; min-width: 0; }
}

/* ---------------- Board settings popover (color + piece style picker) ---------------- */
.setting-group { display: flex; flex-direction: column; gap: 8px; }
.setting-group + .setting-group { margin-top: 16px; }
.setting-label { font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
.swatch-row { display: flex; gap: 8px; }
.swatch {
  width: 28px; height: 28px; border-radius: 10px; padding: 0; cursor: pointer;
  border: 2px solid transparent; overflow: hidden; position: relative;
  background: linear-gradient(135deg, var(--swatch-light) 50%, var(--swatch-dark) 50%);
}
.swatch:hover { transform: translateY(-1px); }
.swatch-active { border-color: var(--navy-900); box-shadow: 0 0 0 2px var(--paper), 0 0 0 4px var(--navy-900); }
.piece-set-row { display: flex; gap: 8px; flex-wrap: wrap; }
.piece-set-btn {
  display: flex; align-items: center; gap: 7px;
  font-family: var(--font-body); font-size: 0.82rem; font-weight: 600; padding: 5px 12px 5px 6px;
  border-radius: 999px; border: 1.5px solid var(--cream-line); background: var(--paper); cursor: pointer; color: var(--text-body);
}
.piece-set-thumb { width: 26px; height: 26px; display: block; filter: drop-shadow(0 1px 1px rgba(0,0,0,0.2)); }
.piece-set-active { border-color: var(--navy-900); background: var(--navy-900); color: var(--cream); }

/* ---------------- Trainer ---------------- */
/* "Fit to screen," every screen size — not just mobile. The training
   screen should never need page-level scrolling: board, chat, and
   controls always fit in whatever height is actually available,
   whether that's a phone in portrait, a tablet, a short laptop lid, or
   a tall desktop monitor. This used to only be true below 640px (see
   the git history / PROGRESS.md — Sessions 19-22 built a genuinely
   good version of this, just scoped to one breakpoint); everything in
   this base rule is that same idea promoted to apply unconditionally,
   with the 860px/640px/560px tiers below now layering PURELY on top
   of it — simplifying what the layout looks like at each width
   (single column vs two, icon-only nav, hidden labels, etc.) rather
   than each one having to separately re-implement "and also don't
   scroll."
   .container becomes the outer flex column (viewport height, header's
   natural size on top); .trainer-shell fills whatever's left
   (flex: 1 1 auto + min-height: 0, the standard "let a flex child
   actually shrink below its content size" pairing); .trainer-layout
   fills what's left of THAT. The board's actual pixel size still
   isn't guessed here via vh/vw math — js/train.js's initBoardFit
   measures .t-board's real rendered box (whatever height the layout
   below computed it down to) and feeds that back as --board-size, for
   the same reason the old mobile-only version did: a fixed guess
   necessarily either underfills most screens or overflows short
   ones, and that's just as true on a laptop as it is on a phone. */
body.trainer-page .container {
  height: calc(100vh - var(--banner-height)); height: calc(100dvh - var(--banner-height));
  display: flex; flex-direction: column; overflow: hidden;
}
/* flex: 0 0 auto (no grow, no shrink) so the header always keeps its
   natural height — .trainer-shell right below is the one flex child
   meant to absorb all the give (grow OR shrink); without this, the
   header would share equally in any shrinking too (flex's default
   flex-shrink is 1 on every item), which could squeeze it on a
   short/awkward viewport instead of leaving the header alone and
   letting the training screen's own internal layout (and, as a last
   resort, the chat log's internal scroll) absorb the difference. */
body.trainer-page .site-header { flex: 0 0 auto; }
/* Third flex child of .trainer-page .container, alongside the header
   and .trainer-shell (see the big comment above this block) -- Phase 6
   added this between them for train.html's guest banner. flex: 0 0 auto
   keeps it at its natural height same as the header, rather than
   competing for shrink space with .trainer-shell on a tight viewport. */
.guest-banner {
  flex: 0 0 auto;
  padding: 6px 12px; margin: 0 0 8px; border-radius: 12px;
  background: var(--cream); border: 1px dashed var(--pencil);
  font-size: 0.8rem; line-height: 1.3; color: var(--text-body); text-align: center;
}
.guest-banner a { color: var(--navy-900); font-weight: 600; }
.guest-banner[hidden] { display: none; }
body.trainer-page .trainer-shell {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column; overflow: hidden;
  padding: 16px 0;
}
body.trainer-page .trainer-head,
body.trainer-page .session-bar-track { flex: 0 0 auto; }
body.trainer-page .trainer-layout { flex: 1 1 auto; min-height: 0; }
/* Both the board and the chat panel should fill the full height of
   whatever cell/row the layout below gives them, rather than shrinking
   to their own natural content size — that's what lets the board's
   real measured box (see initBoardFit) and the chat log's flexible
   1fr row (see the two-column rule below) mean anything. Everything
   else (.t-pill, .t-controls) keeps sizing to its own
   content, which is why .trainer-layout's own align-items:start below
   is left alone rather than changed wholesale. */
body.trainer-page .t-board,
body.trainer-page .t-chat { align-self: stretch; min-height: 0; }
.trainer-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; flex-wrap: wrap; gap: 8px;}
.trainer-head-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
.trainer-head h1 { font-size: 1.6rem; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
/* Current variation's own name, next to the course title — smaller,
   monospace (vs. the h1's serif display font), and colored with this
   course's own accent (--course-accent, set per-course in js/train.js)
   rather than the navy used for headings everywhere else, so it reads
   as "which one, specifically" rather than a second title. Updated
   every time runQueue() loads a new line, including on segment-bar
   jumps (Session 31) and Prev/Next between lines.
   Horizontally scrollable rather than ellipsis-truncated: long names
   (e.g. "Classical Variation, Giuoco Pianissimo, Main Line") still get
   clipped to max-width so they don't blow out the header layout, but
   the trainee can scroll sideways within that fixed-width box to read
   the rest instead of it being cut off with "..." and unreadable.
   padding-bottom reserves real room below the text baseline for the
   scrollbar track (see ::-webkit-scrollbar below), well clear of the
   text descenders. That padding does grow this item's own box, though
   — and .trainer-head-left centers its items (align-items: center), so
   without correcting for it the extra height pushes the *text* upward
   as the taller box gets re-centered in the row (this was reported and
   is a real bug, not a rendering quirk to work around visually). The
   margin-bottom below is the fix: an equal-and-opposite negative
   value, which cancels padding-bottom's contribution to the box's
   margin-box size (what the flex row actually centers against) while
   leaving the padding itself — and the scrollbar track that lives in
   it — rendered exactly as before. Net effect: the row centers this
   item as if it were still its original, unpadded height, so the text
   stays glued to its original position, and only the scrollbar strip
   extends past the box's nominal edge underneath it. Keep these two
   values equal-and-opposite (currently 10px / -10px) if either one is
   ever changed — that's what keeps the text from drifting again. */
.line-name-badge {
  font-family: var(--font-mono); font-size: 0.85rem; font-weight: 600;
  color: var(--course-accent, var(--pencil-dark));
  overflow-x: auto; overflow-y: hidden; white-space: nowrap;
  -webkit-overflow-scrolling: touch; scrollbar-width: thin;
  flex: 0 1 auto; min-width: 0; max-width: 45%;
  padding-bottom: 10px;
  margin-bottom: -10px;
}
.line-name-badge::-webkit-scrollbar { height: 2.5px; }
.line-name-badge::-webkit-scrollbar-track { background: transparent; }
.line-name-badge::-webkit-scrollbar-thumb {
  background: var(--cream-line); border-radius: 4px;
}
/* Prev/next VARIATION (jumps a whole line in the queue) — distinct from
   .nav-arrow-btn below, which steps through MOVE history within the
   current line. Small and round like .back-arrow, just sized down to
   sit inline next to the variation-name badge rather than as its own
   full control row. */
.variation-nav { display: flex; gap: 4px; flex: 0 0 auto; }
.variation-nav-btn {
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 50%; padding: 0;
  font-size: 1rem; line-height: 1; color: var(--navy-900); background: var(--paper);
  border: 1.5px solid var(--cream-line); cursor: pointer;
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
.variation-nav-btn:hover:not(:disabled) { border-color: var(--pencil); background: var(--cream); }
.variation-nav-btn:active:not(:disabled) { transform: scale(0.95); }
.variation-nav-btn:disabled { opacity: 0.35; cursor: default; }
.session-progress { font-family: var(--font-mono); color: var(--text-muted); font-size: 0.85rem; flex: 0 0 auto; }

/* Back arrow: plain, unobtrusive, same row as the course title and
   "Line X of Y" — the trainer screen's only way back once the site
   header is hidden on phones (see the 640px block below), and just a
   quick shortcut everywhere else. */
.back-arrow {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%; text-decoration: none;
  font-size: 1.2rem; color: var(--navy-900); background: var(--paper);
  border: 1.5px solid var(--cream-line); transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
.back-arrow:hover { border-color: var(--pencil); background: var(--cream); }

/* Per-line session bar (Session 31): one clickable segment per
   variation in this notebook, replacing the old single continuous
   fill. Colors mirror the exact vocabulary already used on the
   opening-list cards (js/opening-list.js): orange = practiced/
   "learned", purple = mastered. The current segment overrides both
   with blue — it's meant to answer "where am I", not "have I learned
   this one", so it deliberately doesn't show learned/mastered status
   while it's the active one.
   For courses with a lot of lines (the Sicilian notebook has 250+),
   segments can get down to a few px wide each — `overflow-x: auto`
   lets the strip scroll horizontally rather than becoming unreadable
   or forcing the whole page to reflow. Segments are still individually
   clickable/hoverable at that width; there's no grouping/pagination,
   by design, per the original request ("divided to the number of
   variations that opening has"). */
.session-bar-track {
  display: flex; gap: 2px; height: 8px; margin-bottom: 14px;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; /* the strip is a glanceable/clickable indicator, not a scroll area meant to show a visible scrollbar */
}
.session-bar-track::-webkit-scrollbar { display: none; }
.session-seg {
  flex: 1 1 4px; min-width: 3px; height: 100%; border-radius: 5px;
  border: none; padding: 0; margin: 0; cursor: pointer;
  background: var(--cream-line);
  transition: transform 0.1s var(--ease), background 0.15s var(--ease);
}
.session-seg:hover, .session-seg:focus-visible { transform: scaleY(1.5); outline: none; }
.session-seg-learned { background: var(--pencil); }
.session-seg-mastered { background: var(--violet); }
.session-seg-current { background: var(--current-blue); cursor: default; }
.session-seg-current:hover { transform: none; }

/* Trainer layout, desktop: named grid areas so the board can stay a
   single tall block on the left while everything else — chat, the
   "discovered" pill, the controls cluster, and the settings popover —
   stacks in reading order on the right. See the mobile override below
   for how the SAME children get rearranged (chat above the board,
   controls collapsed into one row underneath it) purely via CSS, no
   DOM/JS changes needed. */
.trainer-layout {
  display: grid;
  grid-template-columns: minmax(260px, 560px) 1fr;
  /* The chat row is the one flexible track (1fr) — it absorbs whatever
     vertical space is left after the pill/controls rows take their own
     natural content height, so the whole right-hand column (and
     therefore the "board" cell, which spans all three of these rows)
     ends up matching whatever the actual viewport allows rather than
     growing past it. .chat-log then fills that row itself (see its own
     flex:1 1 auto; min-height:0 below) and only scrolls internally if a
     conversation genuinely doesn't fit, instead of ever pushing the
     page into needing to scroll.
     There used to be a 4th row here for the Settings/Mode popover
     (.t-settings) — removed: that popover is now `position: fixed`
     (see .settings-popover below), floating above whichever toolbar
     button opened it instead of occupying its own row and pushing the
     board/chat around every time it opened. */
  grid-template-rows: 1fr auto auto;
  column-gap: 36px; row-gap: 14px;
  align-items: start;
  grid-template-areas:
    "board chat"
    "board pill"
    "board controls";
}
.t-chat { grid-area: chat; }
.t-board { grid-area: board; }
.t-pill { grid-area: pill; }
.t-controls { grid-area: controls; }
/* Same `[hidden]` vs. bare-`display` gotcha as `.course-full-list`
   above, caught by auditing for the same pattern rather than a
   separate report — `.trainer-layout` sets `display: grid` (and, on
   narrower breakpoints below, `display: flex`) unconditionally at
   several different specificities, any of which would silently beat
   the browser's default `[hidden] { display: none }`. Only used by
   js/timetrial.js right now (`data-timetrial-play` starts `hidden` in
   timetrial.html and gets toggled with the preset picker) — `!important`
   here, unlike `.course-full-list[hidden]` above, because it needs to
   beat display rules at more than one specificity across the
   mobile/landscape breakpoints further down this file, not just the
   one unconditional rule right above it. */
.trainer-layout[hidden] { display: none !important; }

/* Fluid sizing: the real, authoritative size comes from --board-size,
   set by js/train.js's initBoardFit to the actual measured leftover
   space in .t-board — same reasoning as the (formerly mobile-only)
   fallback below: a fixed vw/vh formula can't know how much room the
   header/chat/controls/current viewport HEIGHT actually left, so it
   either underfills or overflows depending on the screen. The
   min(560px, 92vw, 78vh) is only the same-paint fallback for the
   instant before that JS runs (or if it never runs at all). */
.board-wrap { width: 100%; max-width: var(--board-size, min(560px, 92vw, 78vh)); max-height: 100%; margin: 0 auto; }

.board-grid {
  display: grid; grid-template-columns: repeat(8, 1fr); grid-template-rows: repeat(8, 1fr);
  width: 100%; aspect-ratio: 1;
  border-radius: 16px; overflow: hidden; box-shadow: var(--shadow-lift); border: 5px solid var(--navy-900);
  position: relative; /* anchors .board-arrow-overlay below */
}
.board-square {
  display: flex; align-items: center; justify-content: center; position: relative; cursor: pointer; user-select: none;
  touch-action: none; /* let JS own the gesture entirely — stops mobile browsers from scrolling/zooming mid-drag */
}
.board-square.light { background: var(--board-light); }
.board-square.dark { background: var(--board-dark); }
/* Selected square — outline plus a full-square tint (::before, so it
   paints under the piece <img> and the piece art stays fully visible
   on top, same technique as .hint-square below). Previously outline
   only, which read as just an outline box rather than a highlighted
   square; this keeps the outline as a crisp edge and adds the fill on
   top of it, same accent tone the legal-target dot and drag-hover fill
   already use, so a selected square doesn't introduce a new color the
   board didn't already have elsewhere. */
.board-square.selected { outline: 3px solid var(--pencil); outline-offset: -3px; }
.board-square.selected::before {
  content: ""; position: absolute; inset: 0; background: rgba(226, 145, 76, 0.35); pointer-events: none;
}
.board-square.legal-target::after {
  content: ""; position: absolute; width: 26%; height: 26%; border-radius: 50%; background: rgba(226, 145, 76, 0.55);
}
/* Hovering a legal square while carrying a piece (js/board.js onDragMove) —
   a stronger highlight than the resting legal-target dot so the drop
   target is obvious mid-drag. */
.board-square.drag-hover { outline: 3px solid var(--pencil); outline-offset: -3px; }
.board-square.drag-hover::after { width: 100%; height: 100%; border-radius: 0; background: rgba(226, 145, 76, 0.28); }
/* "Hint" toolbar button (js/train.js) highlights the square of the piece
   that needs to move next, rather than repeating the hint text in chat
   (which was already shown as the pre-move prompt, or reachable via the
   coach's explanation on a wrong guess — restating it in a fresh bubble
   on click added nothing). Set via boardController.showHint(square) /
   cleared via clearHint() (js/board.js) — persists across re-renders
   until the trainee's next move (board.update()) clears it, unlike the
   brief auto-expiring .flash-correct/.flash-wrong below. The tint is a
   ::before (painted before the piece <img> in DOM order) so the piece
   art stays fully visible on top of it. */
.board-square.hint-square { outline: 3px solid var(--current-blue); outline-offset: -3px; }
.board-square.hint-square::before {
  content: ""; position: absolute; inset: 0; background: rgba(63, 125, 201, 0.38); pointer-events: none;
}
/* "Solution" toolbar button (js/train.js) — shown after Hint's already
   been clicked once for the current move. One SVG, rebuilt on every
   board render() (js/board.js's renderArrowOverlay) and appended last
   so it paints above every square/piece. `pointer-events: none` so it
   never intercepts clicks meant for the square underneath — you can
   still click through the arrow to make the move it's pointing at. */
.board-arrow-overlay {
  position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 5;
}
/* Pieces are real SVG artwork (assets/pieces/<set>/<code>.svg), sized as
   a percentage of the square so the whole board scales cleanly at any
   viewport width — no fixed px/rem sizing to fight with. */
.board-square .piece {
  width: 82%; height: 82%; object-fit: contain; pointer-events: none;
  filter: drop-shadow(0 1px 1px rgba(0,0,0,0.25));
}
/* The piece currently being carried (js/board.js `drag`) fades out on its
   origin square so the floating ghost reads as "picked up," not
   duplicated. The ghost itself (.piece-drag-ghost) is a separate <img>
   appended straight to <body> so it isn't clipped by the board and can
   freely follow the pointer past the board's own edges. */
.piece-lifted { opacity: 0.25; }
.piece-drag-ghost {
  position: fixed; z-index: 1000; pointer-events: none;
  filter: drop-shadow(0 3px 6px rgba(0,0,0,0.4));
  transform: scale(1.08);
}
.board-dragging, .board-dragging .board-square { cursor: grabbing; }
.flash-correct { background: #cbe8d7 !important; }
.flash-wrong { background: #f3c9c5 !important; }
/* Last-move highlight: both the from AND to squares of whichever move
   produced the position currently on screen — set via js/board.js's
   lastMoveFrom/lastMoveTo, fed by every boardController.update() call
   (js/trainer.js live play, js/train.js's Prev/Next history nav,
   js/puzzles.js's "correct" reveal). Deliberately background-image (not
   the ::before overlay .hint-square uses) layered on top of the plain
   .light/.dark background-color set above — so it can never fight the
   hint-square tint for the same ::before pseudo-element on the rare
   move where both would apply, and so it paints BELOW the piece <img>
   in stacking order without needing z-index (img is a later sibling in
   DOM order, not this element's own ::before/::after). Color comes from
   --last-move-tint (defined in :root above), not a literal rgba() here,
   because js/board-themes.js overrides that one variable to a warm
   amber when the "Forest Green" board theme is active — the default
   ink-green tint nearly disappears against that theme's own green
   squares. Every other board theme just uses the default green. */
.board-square.last-move { background-image: linear-gradient(var(--last-move-tint), var(--last-move-tint)); }

/* Underpromotion picker — a 2x2 grid of piece choices that replaces
   a promotion target square's normal contents while a choice is
   pending (js/board.js `pendingPromotion`). Clicking any other square
   cancels it; clicking a choice fires the move with that promotion. */
.promo-picker {
  position: absolute; inset: 6%; z-index: 5; display: grid;
  grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 2px;
  background: var(--paper); border: 2px solid var(--navy-900); border-radius: 10px;
  box-shadow: var(--shadow-card); overflow: hidden;
}
.promo-picker-btn {
  display: flex; align-items: center; justify-content: center; padding: 4%;
  border: none; background: transparent; cursor: pointer;
}
.promo-picker-btn:hover, .promo-picker-btn:focus-visible { background: var(--cream-line); outline: none; }
.promo-picker-btn img { width: 100%; height: 100%; object-fit: contain; pointer-events: none; }

/* Prev/Next move-history navigator — steps through positions already
   reached in the current line (js/trainer.js `history`). Next is only
   ever enabled up to the live position, so it can't be used to peek at
   a move the trainee hasn't made yet.
   Desktop: a prominent, labeled two-button bar (see .trainer-controls
   below for why it ends up sitting above the settings/hint toolbar).
   Mobile: collapses to compact icon-only buttons that share one row
   with the toolbar, underneath the board — see the 860px breakpoint. */
.board-nav { display: flex; flex-direction: column; gap: 8px; }
.nav-position { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted); text-align: center; }
.nav-buttons { display: flex; gap: 10px; }
.nav-arrow-btn {
  flex: 1 1 0; display: flex; align-items: center; justify-content: center; gap: 8px;
  height: 52px; border-radius: 16px; border: 2px solid var(--cream-line);
  background: var(--paper); color: var(--navy-900); font-family: var(--font-body);
  font-weight: 700; font-size: 0.95rem; cursor: pointer;
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease), transform 0.1s var(--ease);
}
.nav-arrow-btn:hover:not(:disabled) { border-color: var(--pencil); background: var(--cream); }
.nav-arrow-btn:active:not(:disabled) { transform: scale(0.97); }
.nav-arrow-btn:disabled { opacity: 0.35; cursor: default; }
.nav-arrow-icon { font-size: 1.05rem; line-height: 1; }
.nav-arrow-label { font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; }

/* Wraps the toolbar (settings/hint) and the board-nav (prev/next).
   Desktop: column-reverse puts board-nav (the 2nd DOM child) on top and
   the toolbar underneath it — i.e. prev/next bigger and above
   settings/hint, as asked for. Mobile flips to a single row instead
   (see 860px breakpoint) with normal DOM order, so toolbar lands on the
   left and board-nav on the right, matching a phone-style control bar
   directly under the board. */
.trainer-controls { display: flex; flex-direction: column-reverse; gap: 14px; }

/* File/rank coordinates, drawn inside the edge squares themselves —
   color flips to whatever contrasts with that square's own shade, the
   same convention most online boards use. */
.coord-label {
  position: absolute; font-family: var(--font-mono); font-weight: 700;
  font-size: clamp(0.55rem, 1.6vw, 0.72rem); line-height: 1; pointer-events: none; opacity: 0.75;
}
.coord-file { left: 3px; bottom: 2px; }
.coord-rank { left: 3px; top: 2px; }
.board-square.light .coord-label { color: var(--board-dark); }
.board-square.dark .coord-label { color: var(--board-light); }

/* ---------------- Trainer chat panel ---------------- */
.chat-panel {
  background: var(--cream); border: 1.5px solid var(--cream-line); border-radius: 18px;
  overflow: hidden; display: flex; flex-direction: column;
}
/* Speed control lives here, in its own strip BELOW .chat-log (see the
   markup order in train.html), with a hairline divider so it reads as
   a footer for the conversation rather than floating detached beneath
   it. Only rendered visible when ChatAnimationPref.isEnabled() is
   true — see js/chat-speed-pref.js's updateVisibility(). There's
   nothing to speed up when messages appear in full instantly, so a
   user with word-by-word animation turned off never sees this
   control at all. */
.chat-panel-footer {
  display: none; align-items: center; gap: 16px;
  padding: 10px 14px; border-top: 1px solid var(--cream-line);
  font-family: var(--font-mono); font-size: clamp(0.62rem, 1.6vw, 0.72rem);
  color: var(--text-muted);
}
/* The footer is a shell around two independently-gated groups (Speed —
   animation-preference-gated, Auto-scroll — entitlement-gated, see
   train.html's comments on each). Neither is guaranteed visible at the
   same time as the other, so the footer bar itself only renders when at
   least one direct child isn't [hidden] — otherwise a signed-in Pro
   trainee with word-animation off would see an empty bordered strip
   with nothing in it. */
.chat-panel-footer:has(> .chat-footer-control:not([hidden])) { display: flex; }
.chat-footer-control { display: flex; align-items: center; gap: 8px; }
/* Same [hidden]-vs-bare-display gotcha as .course-full-list[hidden]/
   .filter-panel[hidden]/.pgn-panel[hidden] elsewhere in this file: the
   bare `.chat-footer-control { display: flex }` rule above has the same
   specificity as the browser's default `[hidden] { display: none }` and,
   being an author rule, always wins the tie — so setting `.hidden = true`
   in JS (see js/chat-auto-animate-pref.js's updateVisibility, which
   correctly sets this for every signed-in trainee) had no visual effect
   on its own. This is why the AUTO-ANIMATE checkbox kept rendering (and
   stayed clickable) for signed-in users even though `hidden` was already
   true — it only visually disappeared when the sibling Speed control was
   ALSO hidden, collapsing the whole .chat-panel-footer via the :has()
   rule above as a side effect, not because this control's own hidden
   state was ever actually honored. Needed explicitly, same as those
   other three selectors. */
.chat-footer-control[hidden] { display: none; }
.chat-speed-control-label { text-transform: uppercase; letter-spacing: 0.05em; }
.chat-speed-slider {
  width: clamp(72px, 22vw, 110px); accent-color: var(--navy-600); cursor: pointer;
  /* accent-color alone only reliably paints the THUMB + filled portion
     in every browser — the unfilled track falls back to the browser's
     own UA default, which is a dark gray/black groove. That default
     reads fine on the light theme's cream background but nearly
     disappears against dark mode's near-black background (a black
     track on a near-black panel) — this was the "line for the speed
     is not visible in dark mode" bug. Setting the track color
     explicitly for both engines fixes it in both themes rather than
     relying on the default matching whichever theme happens to be
     active. */
  height: 4px;
}
.chat-speed-slider::-webkit-slider-runnable-track {
  height: 4px; border-radius: 4px; background: var(--cream-line);
}
.chat-speed-slider::-moz-range-track {
  height: 4px; border-radius: 4px; background: var(--cream-line);
}
.chat-speed-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--navy-600); border: none; cursor: pointer;
  margin-top: -4.5px; /* centers the 13px thumb on the 4px track */
}
.chat-speed-slider::-moz-range-thumb {
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--navy-600); border: none; cursor: pointer;
}
.chat-speed-value {
  min-width: 30px; text-align: left; font-weight: 600; color: var(--navy-900);
}
/* Auto-scroll/animate toggle checkbox — same accent-color treatment as
   the .settings-toggle-row checkboxes on profile.html, just smaller to
   fit this compact mono-font footer strip rather than that page's
   larger settings-card rows. */
.chat-auto-animate-toggle {
  width: 14px; height: 14px; accent-color: var(--navy-600); cursor: pointer; margin: 0;
}
.chat-log {
  display: flex; flex-direction: column; gap: 10px;
  /* Top padding is intentionally larger than the other three sides:
     for the very common case of a message landing as the FIRST (only)
     row in a freshly-cleared log — e.g. the opening explanation when a
     new line starts — js/train.js's scroll-based "start" offset
     (CHAT_ROW_SCROLL_START_EXTRA_PX) has nothing to scroll past and
     clamps back to 0, so it can't push that first row down at all.
     Real top padding is what actually creates breathing room above it
     in that specific (first-message) case, which is what several
     rounds of "still too high" feedback turned out to be about — the
     scroll offset above still matters for later messages once there's
     real scroll distance to work with, this is the piece that covers
     the first one. */
  padding: 40px 16px 16px;
  /* flex:1 1 auto + min-height:0 lets this fill whatever height its
     row/cell actually has (see .trainer-layout's grid-template-rows
     and .t-chat's align-self:stretch above) and shrink below its own
     content size when that's tight — the "fit to screen at every
     size" behavior. The max-height stays as an upper cap so it
     doesn't grow absurdly tall with nothing to balance it against on
     a very large monitor; min() keeps it from ever exceeding 46% of
     the viewport's height either, on the off chance the flex-computed
     row is even taller than that. Mobile tiers below override this
     down to a much tighter budget (13dvh) where every pixel counts. */
  flex: 1 1 auto; min-height: 0; max-height: min(420px, 46dvh);
  overflow-y: auto; scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch; /* momentum scroll on older iOS Safari */
  /* Stop a touch-scroll that reaches the top/bottom of this (small,
     often near-full) box from "chaining" into scrolling the page
     behind it — which on the no-scroll trainer (body itself is
     effectively locked, see body.trainer-page) can otherwise feel like a
     stuck/fighting scroll right at the edges instead of a clean stop. */
  overscroll-behavior: contain;
}
.chat-message { display: flex; align-items: flex-start; gap: 10px; }
/* Row entrance (avatar + bubble sliding/fading in together) — applied via
   JS only to messages that should animate at all (see addChatMessage's
   `animate` option in js/train.js). Messages that opt out (the
   already-learned "what's the best move?" challenge prompt, and the
   line-complete congratulation message) get .chat-message-static instead,
   which renders immediately with no motion at all. */
.chat-message-animated { animation: chat-in 0.18s ease-out; }
@keyframes chat-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }

/* Word-cascade reveal: each word of an animated bubble's text fades and
   lifts in with a fixed, deliberately slow, consistent delay between
   words (js/train.js's WORD_REVEAL_STEP_MS) — more suitable than a
   literal character-by-character typewriter, and the pace stays the
   same "readable" speed regardless of message length rather than
   speeding up for longer messages. A message this makes feel too slow
   to wait out can be skipped: clicking/tapping anywhere on the row (see
   .chat-message-revealing below) jumps straight to the finished text. */
/* Transition-based (not @keyframes) on purpose: js/train.js needs to be
   able to change how fast a word fades in WHILE it's already fading —
   an animation-duration change on a running CSS animation restarts its
   timing in some browsers, but a transition-duration change just
   reprices the transition already in flight. `.chat-word-visible` is
   added by JS the instant a word should start revealing; its own
   `transitionDuration` is set inline per-word, read fresh off the
   speed slider at that exact moment (see runWordReveal). */
.chat-word {
  display: inline-block; opacity: 0; transform: translateY(3px);
  transition: opacity 0.32s ease-out, transform 0.32s ease-out;
}
.chat-word-visible { opacity: 1; transform: translateY(0); }

/* While a message's word-cascade is still in progress, js/train.js adds
   this class to the row so the whole row (not just the bubble) shows a
   pointer cursor, signaling it's clickable/tappable to skip ahead — and
   removes it once the cascade finishes on its own, so a fully-revealed
   message doesn't look perpetually clickable. */
.chat-message-revealing { cursor: pointer; }

/* Added to a bubble on click/tap to instantly complete its word-cascade:
   every .chat-word inside jumps straight to its finished state regardless
   of how far into its own delay/animation it currently was. */

/* Fixed avatar image next to every message (assets/coach-avatar.png) —
   replaces the old CSS-generated gradient-initial avatar now that
   there's no per-tab randomized persona to represent (coach-persona.js
   removed). */
.chat-avatar {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%;
  object-fit: cover; background: var(--paper);
  box-shadow: 0 1px 3px rgba(10,23,48,0.25);
}

.chat-bubble {
  font-family: var(--font-body); font-size: 0.92rem; line-height: 1.5;
  background: var(--paper); border-radius: 6px 14px 14px 14px; padding: 10px 14px;
  box-shadow: var(--shadow-card); border-left: 3px solid var(--navy-600); max-width: 100%;
}
.chat-bubble-hint { border-left-color: var(--navy-900); }
.chat-bubble-correct { background: var(--success-bg); border-left-color: var(--ink-green); font-weight: 600; color: var(--success-text-strong); }
.chat-bubble-wrong { background: var(--error-bg); border-left-color: var(--ink-red); color: var(--error-text-strong); }
.chat-bubble-opponent { border-left-color: var(--pencil); color: var(--text-muted); }
.chat-bubble-complete { border-left-color: var(--navy-900); font-weight: 600; color: var(--navy-900); }

/* Manual-mode line-complete controls (js/train.js's
   renderLineCompleteActions) — appended into the chat log right after
   the "Perfect line!"/"Line complete." message, not a separate fixed
   panel, so it reads as the tail end of that same message rather than
   a whole new piece of UI. Left-indented past where the avatar column
   sits above it (34px avatar + 10px gap, matching .chat-message) so
   the buttons visually line up under the bubble text rather than the
   avatar. */
.line-complete-actions { display: flex; align-items: center; gap: 10px; padding-left: 44px; }
.line-complete-next-btn { padding: 8px 18px; font-size: 0.88rem; }
/* Small and deliberately unobtrusive — this is a shortcut for the
   minority who want to flip the setting mid-session, not something
   that should compete with the actual Next button for attention. */
.line-complete-auto-btn {
  background: transparent; border: 1.5px solid var(--cream-line); border-radius: 999px;
  padding: 6px 12px; font-family: var(--font-mono); font-size: 0.7rem; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--text-muted); cursor: pointer; transition: border-color 0.15s var(--ease), color 0.15s var(--ease);
}
.line-complete-auto-btn:hover { border-color: var(--pencil); color: var(--navy-900); }

.empty-state { font-family: var(--font-mono); color: var(--text-muted); padding: 40px 0; }
.session-complete { text-align: center; padding: 60px 0; }
.session-complete h2 { margin-bottom: 10px; }

/* "X/Y discovered" — how many lines in this notebook the trainee has
   trained at least once, computed from user_line_progress rows that
   already exist for this course (js/train.js). Purely a progress
   readout, distinct from the SM-2 mastery math that runs underneath. */
.discovered-pill {
  display: flex; align-items: center; gap: 10px; box-shadow: var(--shadow-card);
  background: var(--violet); color: var(--paper);
  border-radius: 999px; padding: 10px 16px;
  font-family: var(--font-body); font-weight: 600; font-size: 0.85rem;
}
.discovered-icon { font-size: 1rem; line-height: 1; }

/* Task 3: wrapper so "discovered" and "practiced" can share the single
   .t-pill grid slot (.trainer-layout's "pill" area) as two rows/columns
   instead of two grid children stacking on top of each other. */
.pill-row { display: flex; flex-direction: row; flex-wrap: wrap; gap: 8px; }

/* "X/Y practiced" (Task 3) — reuses .discovered-pill's inner structure
   (icon/bar-track/bar-fill/count share the same classes) with its own
   background so it reads as a distinct milestone at a glance: violet
   for "seen at least once", this blue for "2 clean reps in a row". */
.practiced-pill {
  display: flex; align-items: center; gap: 10px; box-shadow: var(--shadow-card);
  background: var(--current-blue); color: var(--paper);
  border-radius: 999px; padding: 10px 16px;
  font-family: var(--font-body); font-weight: 600; font-size: 0.85rem;
}

/* Drill mode's live score pill (drill.html/js/drill.js) — same .t-pill
   grid slot the Learn-mode discovered pill occupies, different content
   (a running score instead of a progress bar) and color (pencil
   orange, echoing the 🔥 icon) so the two modes read as visually
   distinct at a glance. */
.drill-score-pill {
  display: flex; align-items: center; gap: 8px; box-shadow: var(--shadow-card);
  background: var(--pencil); color: var(--paper);
  border-radius: 999px; padding: 10px 16px;
  font-family: var(--font-body); font-weight: 600; font-size: 0.85rem;
}
.drill-score-label { font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.85; }
.drill-score-value { font-size: 1.05rem; font-weight: 700; }
.drill-best-value { margin-left: auto; font-family: var(--font-mono); font-size: 0.76rem; opacity: 0.9; }

/* Time Trial (Task 4) — same .drill-score-pill shape/classes (it
   literally reuses .drill-score-label/.drill-score-value/
   .drill-best-value, see timetrial.html), plus its own color and one
   extra element .drill-score-pill doesn't have: a running clock. Own
   color (current-blue, echoing the ⏱️ icon) so it reads as visually
   distinct from Drill's pencil-orange at a glance, same reasoning
   .practiced-pill picked a different color from .discovered-pill. */
.timetrial-score-pill {
  display: flex; align-items: center; gap: 8px; box-shadow: var(--shadow-card);
  background: var(--current-blue); color: var(--paper);
  border-radius: 999px; padding: 10px 16px;
  font-family: var(--font-body); font-weight: 600; font-size: 0.85rem;
}
.timetrial-clock { font-family: var(--font-mono); font-size: 0.95rem; font-weight: 700; }

/* Time Trial's preset picker (js/timetrial.js renderPresetPicker) —
   shown in place of .trainer-layout before a run starts, so it's
   styled as its own small centered panel rather than trying to fit
   .trainer-layout's chat/board/pill grid shape it doesn't have. */
.timetrial-presets { max-width: 420px; margin: 40px auto; text-align: center; }
.timetrial-presets-intro { color: var(--text-muted); margin-bottom: 20px; }
.timetrial-preset-grid { display: flex; flex-direction: column; gap: 10px; }
.timetrial-preset-btn {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 18px; border-radius: 16px; border: 1.5px solid var(--cream-line);
  background: var(--paper); cursor: pointer; color: var(--text-body); box-shadow: var(--shadow-card);
  font-family: var(--font-body); transition: border-color 0.15s var(--ease), background 0.15s var(--ease), transform 0.15s var(--ease);
}
.timetrial-preset-btn:hover { border-color: var(--pencil); background: var(--cream); transform: translateY(-2px); }
.timetrial-preset-length { font-weight: 700; font-size: 1.05rem; }
.timetrial-preset-best { font-family: var(--font-mono); font-size: 0.76rem; color: var(--text-muted); }
.discovered-bar-track {
  flex: 1 1 auto; height: 6px; border-radius: 999px;
  background: rgba(255,255,255,0.3); overflow: hidden; min-width: 40px;
}
.discovered-bar-fill { height: 100%; border-radius: 999px; background: var(--paper); transition: width 0.3s var(--ease); }
.discovered-count { font-family: var(--font-mono); font-size: 0.78rem; white-space: nowrap; }

/* Compact icon toolbar — replaces the old always-visible settings row.
   Settings opens a popover; Hint asks the trainer for the real
   book-fact explanation on demand (js/trainer.js revealHint()), which
   matters most once a line has graduated to "quiz" mode and the
   pre-move prompt stops teaching automatically (alreadyLearned). */
.trainer-toolbar { display: flex; gap: 10px; }
.toolbar-btn {
  flex: 1 1 0; display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 10px 8px; border-radius: 16px; border: 1.5px solid var(--cream-line);
  background: var(--paper); cursor: pointer; color: var(--text-body);
  font-family: var(--font-body); transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
.toolbar-btn:hover { border-color: var(--pencil); }
.toolbar-btn[aria-expanded="true"] { border-color: var(--navy-900); background: var(--cream); }
.toolbar-btn.in-repertoire-toolbar { border-color: var(--pencil); color: var(--pencil-dark); background: var(--cream); }
.toolbar-icon { font-size: 1.15rem; line-height: 1; }
.toolbar-label { font-family: var(--font-mono); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); }

/* Settings/Mode popover — floats above whichever toolbar button opened
   it (position set inline, per-open, by wireModeAndSettingsPopovers in
   js/mode-picker.js — see that function for why: it needs the real
   rendered size of whatever content is currently inside before it can
   sit flush above the button, and that size changes per tab/content
   swap). `position: fixed` deliberately, not `absolute` — this way it
   isn't clipped by `.container`'s `overflow: hidden` on trainer pages,
   and using the viewport as its containing block is exactly what
   getBoundingClientRect()-based positioning in JS assumes. z-index is
   set well above every other layered element on any page (drag ghost,
   profile-menu dropdown, etc. all sit at 1000 or below) — this should
   always render on top, per the original request. Deliberately no
   `display` property here (only `hidden` toggles that, via the
   attribute's own UA-stylesheet default) — an author rule like
   `display: flex` here would out-specificity that default and the
   popover would never actually hide. */
.settings-popover {
  position: fixed; z-index: 2000;
  background: var(--paper); border: 1px solid var(--cream-line); border-radius: var(--radius-lg);
  padding: 16px; box-shadow: var(--shadow-pop);
  width: min(320px, 92vw); max-height: min(420px, 70vh); overflow-y: auto;
}

/* ---------------- Profile ---------------- */
.profile-card {
  background: var(--paper); border-radius: var(--radius-lg); box-shadow: var(--shadow-card);
  border: 1px solid var(--cream-line);
  padding: 32px; margin-top: 24px;
}
.profile-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; margin: 24px 0; }
.stat-block { text-align: center; }
.stat-num { font-family: var(--font-display); font-size: 2rem; color: var(--navy-900); font-weight: 600; }
.stat-label { font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }

/* ---------------- Billing card (Phase 7) ---------------- */
.billing-plan-line { font-family: var(--font-display); font-size: 1.15rem; font-weight: 600; color: var(--navy-900); margin: 4px 0 2px; }
.billing-sub-line { margin: 0 0 16px; font-size: 0.92rem; line-height: 1.5; }
.billing-actions { display: flex; flex-wrap: wrap; gap: 12px; }

.settings-toggle-row {
  display: flex; align-items: flex-start; gap: 12px; cursor: pointer;
  font-size: 0.95rem; color: var(--navy-900);
}
.settings-toggle-row input[type="checkbox"] {
  margin-top: 3px; width: 18px; height: 18px; accent-color: var(--navy-900); cursor: pointer; flex-shrink: 0;
}
.settings-toggle-row small { font-weight: 400; font-size: 0.8rem; margin-top: 2px; }
/* Second (and any later) settings row in the same card needs its own
   breathing room — fine with just one row, but two flush against each
   other with no gap reads as a single confusing block. */
.settings-toggle-row + .settings-toggle-row { margin-top: 16px; }

/* A settings row driven by a <select> instead of a checkbox
   (js/static-scroll-target-pref.js's "when a coach message appears
   without animating" row) — stacked column instead of
   .settings-toggle-row's side-by-side checkbox+text layout, since a
   dropdown reads better sitting under its own label/description than
   squeezed in next to it. */
.settings-select-row {
  display: flex; flex-direction: column; gap: 8px;
  font-size: 0.95rem; color: var(--navy-900);
}
.settings-select-row select {
  align-self: flex-start; margin-top: 2px; padding: 8px 12px; border-radius: 12px;
  border: 1.5px solid var(--cream-line); font-family: var(--font-body); font-size: 0.9rem;
  background: var(--cream); color: var(--text-body); cursor: pointer;
}
.settings-select-row select:focus { outline: 2px solid var(--pencil); outline-offset: 1px; border-color: var(--pencil); }
.settings-select-row small { font-weight: 400; font-size: 0.8rem; margin-top: 2px; }
.settings-toggle-row + .settings-select-row,
.settings-select-row + .settings-toggle-row { margin-top: 16px; }

/* Brief highlight for a settings row someone was deep-linked to (right
   now: train.html's "Auto" button → profile.html#auto-advance-setting,
   see js/profile.js's highlightAutoAdvanceSetting). A background flash
   that fades back to nothing on its own, rather than a border/outline
   that would otherwise need its own separate removal to avoid looking
   like a permanent, slightly broken box around the row. */
.settings-row-highlight { animation: settings-row-flash 2.2s ease-out; border-radius: 12px; }
@keyframes settings-row-flash {
  0% { background: var(--cream-line); }
  100% { background: transparent; }
}

/* ---------------- Footer ---------------- */
.site-footer { padding: 30px 0 50px; color: var(--text-muted); font-size: 0.85rem; text-align: center; }

/* Simple single-bar footer (about/contact/legal + socials), used on
   index.html and every logged-in page. Replaced the previous 3-part
   footer (tagline strip / logo+tagline+socials block / nav columns /
   separate bottom bar) — this version is just one row: copyright on
   the left, socials centered, About/Contact/Privacy/Terms on the
   right. No logo here anymore (the header already carries the logo
   on every page this footer appears on), so the whole theme-inverted
   logo-swap mechanism from the previous two footer sessions is gone
   too — nothing left to swap.

   `.footer-bar` uses a 3-column CSS grid (`1fr auto auto` — see
   below) rather than flex + `justify-content: space-between`,
   specifically so the socials group is centered in the bar itself,
   not just "the leftover space between two other things" (which
   flexbox's `space-between` would give with 3 items, but only
   incidentally, and it drifts off-center as soon as the left/right
   groups are different widths — which they are, copyright text vs.
   4 nav links). Grid's middle track sizes to its own content and the
   outer tracks share the remaining space evenly, which keeps the
   socials visually centered regardless of how long the copyright or
   link list ends up being. */
.site-footer-full .footer-inner { max-width: 1080px; margin: 0 auto; }
.site-footer-full .footer-bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  column-gap: 24px;
}

/* Band color is the INVERSE of whichever site theme is active: light
   theme -> black band (matches the reference product's own footer),
   dark theme -> the band flips to a light/cream band, so the footer
   always contrasts with the page behind it instead of blending into
   an already-dark page. (Kept from the previous footer sessions —
   this part of the brief never changed, only the layout above it.) */
.site-footer-full {
  margin-top: 40px;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding: 22px calc(50vw - 50%);
  background: #0a0a0a;
  color: #b7bcc7;
}
[data-theme="dark"] .site-footer-full { background: #f2f0e8; color: #4b4f5c; }

.site-footer-full .footer-copyright { font-size: 0.85rem; justify-self: start; }

.site-footer-full .footer-socials { display: flex; gap: 12px; justify-self: center; }
.site-footer-full .footer-social-link {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 999px;
  color: #9ba0ac; transition: color 0.15s var(--ease), background 0.15s var(--ease);
}
.site-footer-full .footer-social-link svg { width: 18px; height: 18px; }
.site-footer-full .footer-social-link:hover { color: var(--pencil); background: rgba(255,255,255,0.06); }
[data-theme="dark"] .site-footer-full .footer-social-link { color: #6b7080; }
[data-theme="dark"] .site-footer-full .footer-social-link:hover { color: var(--pencil-dark); background: rgba(10,10,10,0.06); }

.site-footer-full .footer-links { display: flex; gap: 18px; flex-wrap: wrap; justify-self: end; }
.site-footer-full .footer-links a {
  color: #d8dae2;
  text-decoration: none;
  font-size: 0.85rem;
  white-space: nowrap;
}
[data-theme="dark"] .site-footer-full .footer-links a { color: #383c47; }
.site-footer-full .footer-links a:hover { color: var(--pencil); text-decoration: underline; }

@media (max-width: 640px) {
  /* Below this width, three columns of (copyright / 5 icons / 4 links)
     don't fit on one line without wrapping into an unreadable mess —
     stack them instead, centered, in the same top-to-bottom order the
     reference image reads left-to-right in: copyright, then socials,
     then links. */
  .site-footer-full .footer-bar {
    grid-template-columns: 1fr;
    justify-items: center;
    row-gap: 14px;
    text-align: center;
  }
  .site-footer-full .footer-copyright,
  .site-footer-full .footer-socials,
  .site-footer-full .footer-links { justify-self: center; }
  .site-footer-full .footer-links { justify-content: center; }
}

/* ---------------- Legal / static content pages (about, contact,
   privacy, terms) ---------------- */
.legal-shell { max-width: 720px; margin: 0 auto; padding: 44px 0 20px; }
.legal-shell h1 { font-size: clamp(2rem, 4vw, 2.6rem); margin-bottom: 6px; }
.legal-shell .legal-updated { color: var(--text-muted); font-size: 0.85rem; margin-bottom: 30px; }
.legal-shell h2 { font-size: 1.3rem; margin-top: 2em; }
.legal-shell h3 { font-size: 1.05rem; margin-top: 1.6em; }
.legal-shell p, .legal-shell li { color: var(--text-body); line-height: 1.65; }
.legal-shell ul, .legal-shell ol { margin: 0 0 1em; padding-left: 1.3em; }
.legal-shell li { margin-bottom: 0.4em; }
.legal-shell a { color: var(--pencil-dark); }
.legal-shell strong { color: var(--navy-900); }
.legal-intro { font-size: 1.05rem; color: var(--text-muted); max-width: 56ch; }
.contact-list { list-style: none; padding: 0; margin: 26px 0; display: flex; flex-direction: column; gap: 18px; }
.contact-list li { display: flex; flex-direction: column; gap: 2px; }
.contact-list .contact-label { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); }
.contact-list .contact-value a { font-size: 1.05rem; }
.about-section { margin-top: 2.2em; }
.about-quote { font-family: var(--font-display); font-size: 1.3rem; color: var(--navy-900); border-left: 3px solid var(--pencil); padding-left: 18px; margin: 2em 0; }

/* ---------------- Responsive ---------------- */
@media (max-width: 860px) {
  .hero { grid-template-columns: 1fr; }
  .features { grid-template-columns: 1fr; }
  .profile-stats { grid-template-columns: repeat(2, 1fr); }

  /* Controls row: one row, toolbar (settings/hint) on the left, prev/next
     on the right — normal DOM order, no reversing, unlike the desktop
     column-reverse stack. Shared by both single-column tiers (phone AND
     the wider "middle" width below, which now reuses the phone tier's
     layout wholesale — see that block's comment), so it lives here
     rather than being duplicated in both. */
  .trainer-controls { flex-direction: row; align-items: stretch; gap: 10px; }
  .trainer-toolbar, .board-nav { flex: 1 1 0; }
  .trainer-toolbar { display: flex; gap: 8px; }
  /* Icon-only prev/next at this size — there isn't room for "Previous"/
     "Next" text next to the toolbar, and the position readout becomes
     redundant clutter in a one-row control bar. */
  .board-nav { flex-direction: row; }
  .nav-position { display: none; }
  .nav-buttons { flex: 1 1 auto; gap: 8px; }
  .nav-arrow-btn { height: 46px; }
  .nav-arrow-label { display: none; }

  /* Trainer-page layout (board/chat/controls) no longer has its own
     rules at this tier — it now shares the phone tier's rules below
     (merged from 640px up to 860px, see that block's comment) rather
     than getting a separate, subtly-different "tablet" treatment.
     Two different single-column layouts at two nearby breakpoints
     kept drifting out of sync with each other (this was the third
     time the board ended up too small at this specific width) and
     were extra surface area to keep in sync for no real benefit —
     one flex-column layout that both tiers share is simpler and the
     board sizing (`initBoardFit` in js/train.js) already measures the
     real rendered box either way, so it adapts to the wider viewport
     on its own with no separate formula needed. */
}

@media (max-width: 560px) {
  .container { padding: 0 14px; }
  .board-wrap { max-width: var(--board-size, min(100%, 88vh)); }
  .opening-card { padding: 12px; gap: 14px; }
  .settings-popover { padding: 12px; }
  .setting-group + .setting-group { margin-top: 12px; }
  /* Kept short on purpose (see the single-column tier's chat-log rule
     below, currently 13dvh): this is meant to read as a compact
     "latest message" strip, not a tall chat window competing with the
     board for screen space. */
  .chat-log { max-height: 190px; }
  .toolbar-btn { padding: 8px 6px; }
  .nav-arrow-btn { height: 44px; }
}

/* Single-column trainer screen: on top of the universal "fit to
   screen" base rule above (Session 27's generalization of this), this
   tier gets the most aggressively simplified treatment — the header
   is hidden entirely (no room kept for it, unlike the two-column
   desktop tier above 860px which keeps it), and the layout switches
   from a CSS grid to a flex column with explicit ordering so
   board/chat/controls can be reordered independent of their DOM/
   grid-area order.

   Widened from ≤640px (phones only) to ≤860px: there used to be a
   separate "tablet" single-column layout for 641–860px (CSS Grid-
   based, board sized off the leftover-after-chat space), and it kept
   independently breaking in the exact same way this phone layout
   already solved — most recently the board rendering as a tiny
   square whenever the coach's message was long, because Grid's
   leftover-space approach let a long chat squeeze the board down
   arbitrarily far. Rather than re-solve that a third time for a
   second, subtly-different layout, this tier now reuses the exact
   same flex-column layout phones already use — same ordering, same
   header-hidden treatment, same board-gets-the-real-leftover-space-
   via-flex-grow approach (see .t-board below and initBoardFit in
   js/train.js, which measures the real rendered box regardless of
   viewport width). The one thing that's genuinely different at the
   wider end of this range is width, not treatment: .trainer-layout's
   children all already stretch to the container's full width
   (align-items: stretch, further down), so the chat panel is simply
   as wide as the (now wider, up to 860px) container gives it — no
   separate "wider chat" rule needed, it falls out of reusing the same
   layout on a wider screen.

   Settings is intentionally exempt from the tight vertical budget
   below: it's an on-demand popover, not part of the default view, so
   it's fine if opening it needs a touch of internal scroll — the
   fit-to-screen guarantee is about the screen you land on. */
@media (max-width: 860px) {
  body.trainer-page .site-header { display: none; }

  body.trainer-page .container { padding: 0 10px; }

  /* Height/flex/overflow lock itself now lives in the unconditional
     base rule above — this only needs this tier's delta (tighter
     padding than the 16px the desktop tier gets). */
  body.trainer-page .trainer-shell { padding: 6px 0 6px; }

  body.trainer-page .trainer-head { margin-bottom: 2px; flex: 0 0 auto; }
  body.trainer-page .trainer-head h1 { font-size: 1.05rem; }
  body.trainer-page .trainer-head-left { gap: 6px; }
  body.trainer-page .line-name-badge { font-size: 0.68rem; }
  body.trainer-page .back-arrow { width: 28px; height: 28px; font-size: 1.05rem; }
  body.trainer-page .session-progress { font-size: 0.72rem; }
  body.trainer-page .session-bar-track { margin-bottom: 6px; flex: 0 0 auto; }

  body.trainer-page .trainer-layout {
    /* flex:1 1 auto / min-height:0 already come from the base rule;
       what's phone-specific here is switching AWAY from the grid the
       wider tiers use, to a flex column with explicit per-child order
       below (grid-area doesn't apply once display is flex). */
    display: flex;
    flex-direction: column;
    /* THE FIX (Session 22): the desktop rule above sets `align-items:
       start` for the grid layout. This block changes `display` to
       flex/column but never reset `align-items` — so `start` carried
       over. On a column flex container, `align-items` controls the
       HORIZONTAL cross-axis: `start` meant every child (.t-chat,
       .t-board, .t-controls, ...) shrank to its own content width and
       sat at the left edge instead of filling the screen — the exact
       "small and towards the left" bug. `stretch` is flexbox's actual
       default and is what every child here was already built assuming
       (.board-wrap centers *itself* via margin:auto/flex centering
       inside .t-board, but only once .t-board is actually full-width). */
    align-items: stretch;
    gap: 6px;
  }

  /* Thin and first, ahead of the board — cheap to fit, and reads
     naturally as "here's where you are in this notebook" before
     anything else. */
  body.trainer-page .t-pill {
    order: -1; flex: 0 0 auto; padding: 6px 12px;
  }
  body.trainer-page .discovered-bar-track { display: none; } /* count alone is enough at this size */

  /* The signed-out/non-entitled banner (js/guest-banner.js) sits in
     this same flex column, ahead of the board — on the tight,
     no-scroll mobile layout its footprint comes straight out of the
     board/chat space below it, so it's worth squeezing further than
     the already-compact base rule above: smaller text and tighter
     padding/margin. Deliberately NOT clamped to a single line via
     nowrap/ellipsis — the banner's whole purpose is its trailing
     "Upgrade"/"Create a free account" link (js/guest-banner.js), and
     a hard single-line clamp risks silently cutting that link off on
     a narrow phone instead of just wrapping it to a (still short,
     still cheap) second line. order: -2 keeps it ahead of even
     .t-pill (order -1 right above), reading top-to-bottom as "you're
     on the free tier" → "here's where you are" → the board itself. */
  body.trainer-page .guest-banner {
    order: -2; padding: 4px 10px; margin: 0 0 6px; font-size: 0.7rem;
  }

  /* Explicit order for every remaining child: board first (the thing
     you land on and act on), chat right below it (including its
     speed-toggle header), then the controls row, then the settings
     popover. Needed because flex only falls back to DOM order when
     `order` ties, and .t-chat's markup comes BEFORE .t-board in
     train.html (desktop's CSS-grid areas handle that ordering without
     needing this — see the two-column grid rule above — but this
     block switches to `display: flex`, which doesn't use grid-area at
     all, so each child needs its own explicit order here instead). */
  body.trainer-page .t-board { order: 0; }
  /* FIXED height, not content-sized (Session 33 board-stability fix):
     this used to be `flex: 0 1 auto`, which gives the panel a
     content-based basis — its rendered height was just whatever the
     coach message needed, capped from above by .chat-log's max-height
     but with nothing stopping it from being SMALLER for a short
     message. Since .t-board (flex: 1 1 auto, right below) always
     absorbs whatever's left in the column, that meant the board
     visibly changed size message-to-message: a short "what's the best
     move?" challenge prompt (lines already learned) left more room and
     a bigger board, while a longer pre-move hint/explanation (lines
     not yet learned) ate into that room and shrank the board — the
     exact resize the user was seeing. Pinning this to a fixed basis
     (matching .chat-log's own 13dvh cap below) makes the chat panel's
     footprint constant regardless of message length, so .t-board's
     leftover space — and therefore --board-size from initBoardFit —
     no longer moves at all as the coach talks. Longer messages simply
     scroll inside the fixed box instead (.chat-log already has
     overflow-y: auto from the unconditional base rule). `0 0` (no
     grow, no shrink) instead of `0 1` so nothing can still squeeze it
     smaller than this either. */
  body.trainer-page .t-chat { order: 1; flex: 0 0 13dvh; min-height: 0; }
  body.trainer-page .t-controls { order: 2; }
  /* Vertical space in this compact, no-scroll mobile layout is
     precious (see .chat-log's 13dvh max-height right below), so the
     speed-control footer compacts to one tight row — slider and value
     inline, label hidden — rather than being dropped entirely. It
     stays present (the request was for it to exist on mobile too, not
     just resize away), it just costs less vertical space. Note this
     eats into the fixed 13dvh budget set on .t-chat above (.chat-log
     flex-grows to fill whatever that leaves) rather than adding on top
     of it — so toggling the speed control on/off in Settings is the
     only thing that still changes the chat panel's height on this
     tier, never the message content itself. */
  body.trainer-page .chat-panel-footer { padding: 4px 10px; gap: 6px; }
  /* Only the Speed control's label collapses at this width — the
     slider + "1×" value alone still read fine without it. The
     Auto-Animate control (data-auto-animate-control, shown only to
     signed-out visitors — see js/chat-auto-animate-pref.js) has NO
     other content besides its checkbox, so hiding its label here left
     a bare, unlabeled checkbox with no indication of what it does —
     that was the actual mobile bug: this rule used to target the
     shared .chat-speed-control-label class both controls' labels use,
     silently hiding BOTH. Scoped to [data-chat-speed-control] only now
     so Auto-Animate keeps its "Auto-Animate" label at every width. */
  body.trainer-page [data-chat-speed-control] .chat-speed-control-label { display: none; }
  body.trainer-page .chat-speed-slider { width: 15vw; min-width: 46px; }
  /* max-height here is now just a same-value safety cap (the real
     limit comes from the fixed-height parent above) — kept so this
     rule still makes sense read on its own. */
  body.trainer-page .chat-log { max-height: 13dvh; padding: 20px 12px 10px; gap: 6px; }

  /* Board gets whatever's left — centered in its share of the column.
     The actual size comes from --board-size, set by
     initBoardFit() in train.js to the real measured leftover
     space in .t-board (see that function for why: a fixed vh guess
     here can't know how much room the header/chat/controls actually
     left, so it either underfills on most phones or overflows on
     short/landscape ones). The min(92vw, 42dvh) is only a same-paint
     fallback for the instant before JS runs. */
  body.trainer-page .t-board {
    flex: 1 1 auto; min-height: 0;
    display: flex; align-items: center; justify-content: center;
  }
  body.trainer-page .board-wrap { max-width: var(--board-size, min(92vw, 42dvh)); }

  body.trainer-page .t-controls { flex: 0 0 auto; }
  body.trainer-page .nav-arrow-btn { height: 40px; }
  body.trainer-page .toolbar-btn { padding: 6px 6px; }
}

/* Landscape phones: the SAME single-column stack above (still matched
   here too, since it's keyed on width and a landscape phone is often
   still ≤860px wide) is the wrong layout the moment height, not width,
   is the scarce resource. Rotating a phone sideways keeps plenty of
   width but leaves maybe 320-430px of height total — stacking
   board-then-chat-then-controls vertically in that squeezes every
   piece into a sliver while leaving big empty margins on either side
   of the board, which is the "horizontal layout is crap" bug. Keyed
   on actual short height + landscape orientation together (not a
   width breakpoint) because that's the real constraint, and it works
   for both small and large phones without needing to know their exact
   width. Deliberately placed AFTER the single-column block above so
   it wins the cascade for the properties it overrides.

   The fix is to stop stacking and go back to a side-by-side split —
   board on the left where it can use the now-generous height, chat +
   pill + controls + settings in a narrower column on the right — which
   is exactly what the UNCONDITIONAL two-column grid rule far above
   (`.trainer-layout`'s base rule, normally only reached above 860px)
   already does. So this block doesn't invent a new layout: it just
   switches `.trainer-layout` back to `display: grid` with that same
   `grid-template-areas`, tuned to a narrower first column (phones in
   landscape don't have 860px-desktop's width to spare) and tighter
   gaps, and leaves the header hidden + everything else's sizing to the
   single-column block's rules that still apply (chat-panel-footer
   padding, pill/settings compacting, etc.) — only the parts that
   genuinely need to differ for a side-by-side layout are re-declared
   here (chat-log's height cap, board/chat's own display mode, and the
   controls row, which reads better narrow-and-stacked here than the
   single row it is in the portrait phone layout). */
@media (orientation: landscape) and (max-height: 500px) {
  body.trainer-page .site-header { display: none; }
  body.trainer-page .container { padding: 0 10px; }
  body.trainer-page .trainer-shell { padding: 6px 0; }
  body.trainer-page .trainer-head { margin-bottom: 2px; }
  body.trainer-page .trainer-head h1 { font-size: 1rem; }
  body.trainer-page .line-name-badge { font-size: 0.65rem; }
  body.trainer-page .session-bar-track { margin-bottom: 4px; }

  body.trainer-page .trainer-layout {
    display: grid;
    /* Narrower than the desktop rule's minmax(260px, 560px) — a phone
       in landscape rarely has 560px to give the board before the chat
       column gets crushed, so this caps out much sooner and leans on
       the percentage for anything in between. */
    grid-template-columns: minmax(150px, 40%) 1fr;
    grid-template-rows: 1fr auto auto;
    column-gap: 12px; row-gap: 6px;
    align-items: stretch;
    /* grid-template-areas + each child's grid-area come from the
       UNCONDITIONAL base rule above (.t-chat/.t-board/.t-pill/
       .t-controls each already declare their own grid-area outside any
       media query) — no need to repeat them here, only the track
       sizing changes. */
  }
  /* .t-board/.t-chat already get align-self:stretch + min-height:0
     from the unconditional base rule; this just re-centers the board
     inside its (now grid, not flex-grown) cell — same visual result,
     different layout mode underneath. */
  body.trainer-page .t-board {
    display: flex; align-items: center; justify-content: center; min-width: 0;
  }
  body.trainer-page .t-chat { display: flex; flex-direction: column; min-width: 0; }
  /* The portrait phone tier caps this at 13dvh because the board sits
     ABOVE it and needs that height back. Side-by-side, the chat
     column isn't competing with the board for the same axis anymore —
     it can fill its own grid row (1fr) instead, so let it, and scroll
     internally only if a message genuinely doesn't fit. */
  body.trainer-page .chat-log { max-height: none; flex: 1 1 auto; }
  body.trainer-page .t-pill { padding: 4px 10px; }
  body.trainer-page .discovered-bar-track { display: none; }
  /* Narrow column reads better as toolbar-above-nav (like desktop's
     stack) than the single-row layout the portrait/tablet tier uses —
     there usually isn't room for both side by side in a column this
     narrow. */
  body.trainer-page .trainer-controls { flex-direction: column-reverse; gap: 8px; }
  body.trainer-page .trainer-toolbar { flex-direction: row; }
  body.trainer-page .board-nav { flex-direction: column; gap: 4px; }
  body.trainer-page .nav-position { display: block; font-size: 0.68rem; }
  body.trainer-page .nav-buttons { flex-direction: row; }
  body.trainer-page .nav-arrow-btn { height: 34px; }
  body.trainer-page .nav-arrow-label { display: none; }
  body.trainer-page .toolbar-btn { padding: 5px 6px; }
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  .btn, .course-card { transition: none; }
  .chat-message-animated { animation: none; }
  .chat-word { transition: none; opacity: 1; transform: none; }
}

/* Keyboard focus visibility */
a:focus-visible, button:focus-visible, .board-square:focus-visible {
  outline: 3px solid var(--pencil); outline-offset: 2px;
}

/* ---------------- Leaderboards (leaderboard.html) ----------------
   Mode/duration/period pill-tabs + a ranked table. New page, new
   dedicated block rather than reusing .timetrial-preset-btn — that
   one's built for a tall stacked "pick one and go" list; these are
   compact inline toggles meant to sit in a row and be flipped
   between repeatedly while the table underneath re-renders. */
.leaderboard-shell { max-width: 720px; margin: 0 auto; padding: 32px 0 60px; }
.leaderboard-shell h1 { margin-bottom: 4px; }
.leaderboard-subhead { color: var(--text-muted); margin-bottom: 24px; }

.leaderboard-controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 22px; }

.leaderboard-course-select {
  font-family: var(--font-body); font-weight: 600; font-size: 0.92rem; color: var(--navy-900);
  background: var(--paper); border: 1.5px solid var(--cream-line); border-radius: 999px;
  padding: 9px 16px; cursor: pointer;
}

.leaderboard-tabs { display: flex; gap: 6px; background: var(--cream); border-radius: 999px; padding: 4px; }
.leaderboard-tabs button {
  font-family: var(--font-body); font-weight: 600; font-size: 0.86rem; color: var(--text-muted);
  background: transparent; border: none; border-radius: 999px; padding: 7px 14px; cursor: pointer;
  transition: background 0.15s var(--ease), color 0.15s var(--ease);
}
.leaderboard-tabs button:hover { color: var(--navy-900); }
.leaderboard-tabs button.is-active { background: var(--navy-900); color: var(--cream); }
.leaderboard-tabs-sub button.is-active { background: var(--pencil); color: var(--navy-950); }

.leaderboard-my-rank {
  font-family: var(--font-mono); font-size: 0.92rem; color: var(--navy-900);
  background: var(--cream); border: 1.5px dashed var(--cream-line); border-radius: var(--radius);
  padding: 12px 16px; margin-bottom: 16px;
}
.leaderboard-my-rank strong { color: var(--pencil-dark); }

.leaderboard-table-wrap { border: 1px solid var(--cream-line); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow-card); }
.leaderboard-table { width: 100%; border-collapse: collapse; background: var(--paper); }
.leaderboard-table th {
  text-align: left; font-family: var(--font-mono); font-size: 0.76rem; text-transform: uppercase;
  letter-spacing: 0.03em; color: var(--text-muted); background: var(--cream);
  padding: 10px 16px; border-bottom: 1.5px solid var(--cream-line);
}
.leaderboard-table td { padding: 11px 16px; border-bottom: 1px solid var(--cream-line); font-family: var(--font-body); }
.leaderboard-table tr:last-child td { border-bottom: none; }
.leaderboard-rank { font-family: var(--font-mono); font-weight: 700; width: 60px; }
.leaderboard-score { font-family: var(--font-mono); font-weight: 700; color: var(--navy-900); }
.leaderboard-date { color: var(--text-muted); font-size: 0.86rem; white-space: nowrap; }
.leaderboard-row-you { background: var(--success-bg); }
.leaderboard-you-tag {
  font-family: var(--font-mono); font-size: 0.68rem; text-transform: uppercase; color: var(--ink-green);
  border: 1px solid var(--ink-green); border-radius: 999px; padding: 1px 6px; margin-left: 4px;
}

@media (max-width: 640px) {
  .leaderboard-controls { flex-direction: column; align-items: stretch; }
  .leaderboard-tabs { justify-content: center; }
  .leaderboard-table th:nth-child(4), .leaderboard-table td:nth-child(4) { display: none; }
}

/* ============================================================
   Admin panel — admin.html / js/admin.js
   Deliberately given its own visual identity (violet ribbon instead
   of the pencil-orange accent used everywhere else) so it's never
   mistaken for a regular trainee page — see PROGRESS.md's admin
   session notes for why violet was picked (it's already a token,
   --violet/--violet-dark, previously only used sparingly, and reads
   as distinct from both the orange "action" accent and the red/green
   status colors used for pass/fail feedback elsewhere).
   ============================================================ */

/* Small red notification-count circle next to the "Admin" dropdown
   link — same idea as a phone app's unread badge. Hidden by default
   (both via the `hidden` attribute in markup and here as a fallback)
   and only unhidden by js/nav.js's revealAdminLink() when there's a
   nonzero pending-submissions count. */
/* IMPORTANT: only apply flex layout when the link is actually shown.
   An earlier version of this rule was `display: flex !important`
   unconditionally, which overrode the `hidden` attribute nav.js relies
   on to keep this link admin-only — every logged-in user (not just
   admins) saw "Admin" in the dropdown as a result. `:not([hidden])`
   is what actually respects the hidden state; do not remove it or
   re-add !important here. */
.admin-menu-link:not([hidden]) { display: flex; align-items: center; gap: 8px; }
.admin-badge-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
  background: var(--ink-red); color: #fff; font-family: var(--font-mono);
  font-size: 0.68rem; font-weight: 700; line-height: 1;
}
.admin-badge-count[hidden] { display: none; }

.admin-ribbon {
  margin-top: 20px; padding: 18px 22px; border-radius: var(--radius);
  background: linear-gradient(120deg, var(--violet-dark), var(--violet));
  color: #fff; display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}
.admin-ribbon-title { font-family: var(--font-display); font-size: 1.4rem; font-weight: 600; }
.admin-ribbon-sub { font-family: var(--font-body); font-size: 0.88rem; opacity: 0.85; margin-top: 2px; }
.admin-ribbon-back {
  font-family: var(--font-mono); font-size: 0.8rem; color: #fff; text-decoration: none;
  border: 1.5px solid rgba(255, 255, 255, 0.55); border-radius: 999px; padding: 7px 14px;
  white-space: nowrap; transition: background 0.15s var(--ease);
}
.admin-ribbon-back:hover { background: rgba(255, 255, 255, 0.15); }

.admin-tabs {
  display: flex; gap: 6px; margin: 18px 0; border-bottom: 1.5px solid var(--cream-line);
  overflow-x: auto;
}
.admin-tab-btn {
  font-family: var(--font-body); font-weight: 600; font-size: 0.92rem;
  background: transparent; border: none; padding: 10px 16px; cursor: pointer;
  color: var(--text-muted); border-bottom: 2.5px solid transparent; white-space: nowrap;
}
.admin-tab-btn:hover { color: var(--navy-900); }
.admin-tab-btn.active { color: var(--violet-dark); border-bottom-color: var(--violet); }
.admin-tab-btn .admin-badge-count { margin-left: 6px; }

.admin-panel[hidden] { display: none; }

.admin-stat-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin-bottom: 22px; }
.admin-stat-card {
  background: var(--paper); border-radius: var(--radius); box-shadow: var(--shadow-card);
  padding: 18px; text-align: center;
}
.admin-stat-num { font-family: var(--font-display); font-size: 1.9rem; font-weight: 700; color: var(--violet-dark); }
.admin-stat-label { font-family: var(--font-mono); font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); margin-top: 4px; }

.admin-toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
.admin-toolbar input[type="search"] {
  flex: 1; min-width: 200px; padding: 10px 14px; border-radius: 12px;
  border: 1.5px solid var(--cream-line); background: var(--cream); font-family: var(--font-body);
}
.admin-filter-btn {
  font-family: var(--font-mono); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.03em;
  padding: 7px 12px; border-radius: 999px; border: 1.5px solid var(--cream-line);
  background: var(--paper); color: var(--text-muted); cursor: pointer;
}
.admin-filter-btn.active { background: var(--violet); border-color: var(--violet); color: #fff; }

.admin-table-wrap { overflow-x: auto; }
.admin-table { width: 100%; border-collapse: collapse; }
.admin-table th {
  text-align: left; font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: 0.03em; color: var(--text-muted); background: var(--cream);
  padding: 10px 12px; border-bottom: 1.5px solid var(--cream-line); white-space: nowrap;
}
.admin-table td { padding: 11px 12px; border-bottom: 1px solid var(--cream-line); font-size: 0.92rem; vertical-align: top; }
.admin-table tr:last-child td { border-bottom: none; }
.admin-table-username { font-weight: 600; }
.admin-table-email { color: var(--text-muted); font-size: 0.84rem; }

.admin-row-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.admin-row-actions .btn { padding: 6px 12px; font-size: 0.82rem; }
.admin-btn-approve { background: var(--ink-green); color: #fff; border-color: var(--ink-green); }
.admin-btn-approve:hover { background: #3c7256; }
.admin-btn-reject { background: transparent; border: 1.5px solid var(--ink-red); color: var(--ink-red); }
.admin-btn-reject:hover { background: var(--error-bg); }

.admin-variations {
  font-family: var(--font-mono); font-size: 0.82rem; background: var(--cream);
  border-radius: 12px; padding: 10px 12px; margin-top: 8px; white-space: pre-wrap;
  max-height: 160px; overflow-y: auto; border: 1px solid var(--cream-line);
}

.admin-admin-toggle {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  font-family: var(--font-mono); font-size: 0.78rem;
}

.admin-empty-state { text-align: center; padding: 40px 20px; color: var(--text-muted); }

.admin-denied {
  min-height: 60vh; display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: 14px;
}

@media (max-width: 860px) {
  .admin-stat-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
  .admin-stat-grid { grid-template-columns: 1fr; }
  .admin-table-email { display: none; }
}
