/* ════════════════════════════════════════════════════════════
   AcademeX — canonical education-components stylesheet
   THE single source of truth for every learning-material surface:
   static chapters, dynamic chapters/labs (courses/_shared/), the
   course hub, Code Lab & tools, practice, and grades.
   Rendered reference: docs/visualizations/lesson-components/
   Managed via: admin/appearance.html (design tokens; see
   shared/theme-loader.js — tokens below may be overridden at
   runtime from Firestore settings/theme).

   Accent: each page declares its own course accent in its inline
   <style> (or the dynamic renderers set it from the course doc):
     :root { --accent: #ef4444; --accent-rgb: 239,68,68; }
   The values below are safe defaults so the file works standalone.
   ════════════════════════════════════════════════════════════ */

:root {
  --bg: #f6f7f9;
  --bg2: #f8fafc;
  --bg3: #eef1f5;
  --card: #ffffff;
  --border: #e6e9ef;
  --accent: #5b47e0;        /* default — pages/renderers override per course */
  --accent-rgb: 91,71,224;  /* default — pages/renderers override per course */
  --accent2: #0284c7;
  --accent3: #10b981;
  --accent4: #7c3aed;
  --accent5: #f97316;
  --text: #0f1420;
  --text2: #4a5262;
  --text3: #8b94a3;
  --text4: #c3cad6; /* faint text: durations, carets, kbd hints, pace ticks */
  /* Semantic status tokens (correct/wrong/caution across quiz,
     info boxes, feedback, badges) */
  --ok: #10b981;
  --ok-rgb: 16,185,129;
  --err: #ef4444;
  --err-rgb: 239,68,68;
  --warn: #f59e0b;
  --warn-rgb: 245,158,11;
  /* Code-block typography (admin-editable via settings/theme →
     shared/theme-loader.js). Single source of truth for every runnable /
     example code block; individual blocks must NOT set inline font-size,
     line-height, or padding. Compact vars drive .code-block--compact
     (dense comparison grids / micro-snippets) and are not admin-exposed. */
  --code-font-size: 14px;
  --code-line-height: 1.7;
  --code-padding: 20px;
  --code-font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
  --code-font-size-compact: 13px;
  --code-padding-compact: 12px;
  /* Typography tokens (admin-editable via settings/theme →
     shared/theme-loader.js since 2026-07-18). Defaults = the course-page
     stacks (Bricolage Grotesque / Inter / JetBrains Mono); pages no longer
     carry inline :root font overrides — theme-loader's inline vars on
     <html> would beat them anyway. Every offered family must be in the
     canonical Google Fonts <link> the education pages load. */
  --font-display: 'Bricolage Grotesque', 'Segoe UI', 'Roboto', sans-serif;
  --font-body: 'Inter', 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
  --font-mono: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
}

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

body {
  background: var(--bg);
  color: var(--text2);
  font-family: var(--font-body, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif);
  /* 15px at the default 18px root — rem so the A+/A− control
     (shared/font-size.js sets the ROOT size) still scales content. */
  font-size: 0.8333rem;
  min-height: 100vh;
  overflow-x: hidden;
  line-height: 1.62;
}

p { margin-bottom: 16px; line-height: 1.7; font-size: 1em; }
h3 { font-size: 1.6em; font-weight: 700; margin-bottom: 16px; margin-top: 24px; color: var(--text); }
h4 { font-size: 1.3em; font-weight: 600; margin-bottom: 12px; margin-top: 20px; color: var(--text); }
ul, ol { margin-bottom: 16px; padding-inline-start: 32px; }
li { margin-bottom: 10px; line-height: 1.7; font-size: 1em; }
strong { font-weight: 700; color: var(--text); }

code {
  font-family: var(--font-mono, 'Consolas', 'Monaco', 'Courier New', monospace);
  font-size: 0.92em;
  padding: 2px 6px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--accent);
}

body::before {
  content: '';
  position: fixed;
  inset: 0;
  background-image:
    linear-gradient(rgba(var(--accent-rgb),0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(var(--accent-rgb),0.05) 1px, transparent 1px);
  background-size: 40px 40px;
  pointer-events: none;
  z-index: 0;
}
/* Chapter-page backdrop is admin-selectable via the --bg-grid theme token
   (Appearance → Layout & Backdrop → "Chapter page background"):
   0 (default) = the flat course-page design; 1 = the classic 40px square
   grid; fractional values fade it. Pages without the nav chrome keep the
   classic texture unconditionally (and browsers without :has() harmlessly
   keep it everywhere). */
body:has(.chapter-layout.has-nav)::before { opacity: var(--bg-grid, 0); }

#progressBar {
  position: fixed;
  top: 0; inset-inline-start: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--accent), var(--accent4));
  z-index: 999;
  transition: width 0.15s;
  width: 0%;
}

header {
  position: relative;
  z-index: 10;
  padding: 40px 60px 32px;
  border-bottom: 1px solid var(--border);
}

/* Lesson kicker line ("Chapter 1 · Lesson 2 of 6"). With the nav chrome,
   _courseNav.js rewrites it as accent chapter + dot + muted position spans;
   without it, the renderer's raw chapter id shows in the same style. */
.chapter-tag {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 15px;
  color: var(--accent);
  font-weight: 600;
  margin-bottom: 14px;
}
.chapter-tag .kicker-dot {
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--text4); flex: none;
}
.chapter-tag .kicker-pos { color: var(--text3); font-size: 14px; font-weight: 400; }

h1 {
  font-family: var(--font-body, inherit);
  /* The vw term is viewport-dependent — pinned to the cap on a frozen slide
     canvas (body.slide-layout, Present-mode block below). */
  font-size: clamp(1.7rem, 3vw, 2.125rem);   /* → 34px at the mockup width */
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--text);
  margin-bottom: 0;
}

/* Lesson subtitle (section subtitle under the h1) + meta row (read time /
   status). Filled by _courseNav.js updateLessonHeader(); hidden otherwise. */
.lesson-sub {
  font-size: 17px;
  color: var(--text2);
  margin: 14px 0 0;
  line-height: 1.65;
}
.lesson-sub[hidden], .lesson-meta[hidden] { display: none; }
.lesson-meta {
  display: flex;
  align-items: center;
  gap: 22px;
  border-top: 1px solid var(--border);
  margin-top: 26px;
  padding: 16px 0;
  font-size: 14.5px;
  color: var(--text2);
}
.lesson-meta-item { display: inline-flex; align-items: center; gap: 8px; }
.lesson-meta-item svg { flex: none; stroke: var(--text2); }
.lesson-status { margin-inline-start: auto; display: inline-flex; align-items: center; gap: 8px; }
.lesson-status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text4); flex: none; }
.lesson-status.reading .lesson-status-dot,
.lesson-status.done .lesson-status-dot { background: var(--ok); }

.subtitle {
  color: var(--text2);
  font-size: 1.1rem;
  font-weight: 400;
  line-height: 1.6;
}

.back-link {
  display: inline-block;
  font-family: var(--font-mono, 'Consolas', 'Monaco', 'Courier New', monospace);
  font-size: 13px;
  color: var(--text3);
  text-decoration: none;
  margin-bottom: 16px;
  letter-spacing: 0.5px;
}
.back-link:hover { color: var(--accent); }

/* Pill section nav. NOT chapter chrome — chapters navigate through the
   course sidebar / lesson topbar, and their pill bar was deleted with the
   classic layout (2026-08-05). These two rules now belong to the standalone
   student pages that still switch sections in place:
   courses/practice/index.html and courses/tools/index.html. */
.nav-bar {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(255,255,255,0.95);
  backdrop-filter: blur(20px);
  border-bottom: 2px solid var(--border);
  box-shadow: 0 2px 8px rgba(0,0,0,0.05);
  display: flex;
  gap: 8px;
  padding: 16px 60px;
  overflow-x: auto;
}
.nav-bar::-webkit-scrollbar { height: 0; }

.nav-pill {
  flex-shrink: 0;
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
  font-size: 14px;
  letter-spacing: 0.5px;
  padding: 10px 20px;
  border-radius: 6px;
  cursor: pointer;
  border: 2px solid transparent;
  color: var(--text3);
  transition: all 0.2s;
  white-space: nowrap;
  background: none;
  font-weight: 500;
}
.nav-pill:hover { color: var(--text); border-color: var(--border); background: var(--bg2); }
.nav-pill.active { color: var(--accent); border-color: var(--accent); background: rgba(var(--accent-rgb),0.1); font-weight: 600; }

main {
  position: relative;
  z-index: 1;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 60px 80px;
}

.section { display: none; animation: fadeSlide 0.4s ease both; }
.section.active { display: block; }

@keyframes fadeSlide {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes shake {
  0%,100% { transform: translateX(0); }
  15%     { transform: translateX(-7px); }
  30%     { transform: translateX(7px); }
  45%     { transform: translateX(-5px); }
  60%     { transform: translateX(5px); }
  75%     { transform: translateX(-3px); }
  90%     { transform: translateX(3px); }
}

.section-header {
  padding: 52px 0 32px;
  display: flex;
  align-items: flex-start;
  gap: 24px;
}

.section-icon {
  width: 56px; height: 56px;
  border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.6rem;
  flex-shrink: 0;
  border: 1px solid currentColor;
}

.section-title h2 { font-size: 2.25rem; font-weight: 700; margin-bottom: 12px; color: var(--text); }
.section-title p { color: var(--text2); font-size: 1.1rem; line-height: 1.7; }

.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 26px;
  margin-bottom: 28px;
  box-shadow: 0 1px 2px rgba(15,20,32,.03), 0 20px 46px -34px rgba(15,20,32,.3);
}

.card-title {
  font-family: var(--font-mono, 'Consolas', 'Monaco', 'Courier New', monospace);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin-bottom: 20px;
  color: var(--text3);
  font-weight: 500;
}

/* Auto-wrapping tracks: columns collapse on the CONTAINER's own width (not the
   viewport), so they stack correctly inside the narrowed reading column between
   the course-nav sidebar and the right rail. min(100%, …) keeps a lone narrow
   container at a single full-width column instead of overflowing. */
.two-col   { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: 24px; margin-bottom: 24px; }
.three-col { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 20px; margin-bottom: 24px; }
/* min-width:0 on every column (grid children default to min-width:auto =
   min-content, so a wide <pre> code block would force the track past the
   container and keep two columns "fitting"). Applies to hand-authored `html`
   blocks whose columns are bare <div>s, not just the renderer's wrappers. */
.two-col > *, .three-col > * { min-width: 0; }

@media (max-width: 768px) {
  main   { padding: 0 20px 60px; }
  header { padding: 32px 20px 24px; }
  .nav-bar { padding: 12px 20px; }
  /* :not(.slide-layout) — a frozen slide canvas is laid out at its own fixed
     width and must never re-flow on the real viewport (see the Present-mode
     block at the bottom of this file). */
  body:not(.slide-layout) .two-col { grid-template-columns: 1fr; }
}

/* Code blocks (shared across all chapters) */
.code-block {
  background: var(--code-bg, var(--bg2));
  border-top:    var(--code-bd-t-w, 1px) solid var(--code-bd-t-c, var(--border));
  border-right:  var(--code-bd-r-w, 1px) solid var(--code-bd-r-c, var(--border));
  border-bottom: var(--code-bd-b-w, 1px) solid var(--code-bd-b-c, var(--border));
  border-left:   var(--code-bd-l-w, 1px) solid var(--code-bd-l-c, var(--border));
  border-radius: var(--code-radius, 8px);
  padding: var(--code-padding, 20px);
  font-family: var(--code-font-family, 'Consolas', 'Monaco', 'Courier New', monospace);
  font-size: var(--code-font-size, 14px);
  line-height: var(--code-line-height, 1.6);
  overflow-x: auto;
  position: relative;
  white-space: pre;
  display: block;
  word-wrap: normal;
  letter-spacing: 0.3px;
  margin: var(--code-mt, 0) var(--code-mr, 0) var(--code-mb, 20px) var(--code-ml, 0);
  color: var(--code-fg, #1f2937);
}
/* Compact variant for dense side-by-side comparison grids and inline
   micro-snippets. Use this class instead of ad-hoc inline font-size. */
.code-block--compact {
  font-size: var(--code-font-size-compact, 13px);
  padding: var(--code-padding-compact, 12px);
}
.code-block .lang-tag {
  position: absolute;
  top: 12px; inset-inline-end: 16px;
  font-size: 10.5px;
  letter-spacing: 0.14em;
  color: var(--text3);
  text-transform: uppercase;
  font-weight: 600;
}

.kw   { color: #7c3aed; font-weight: 700; }
.cls  { color: #0284c7; font-weight: 700; }
.met  { color: #0891b2; font-weight: 600; }
.str  { color: #16a34a; font-weight: 500; }
.cmt  { color: #94a3b8; font-style: italic; }
.num  { color: #e11d48; font-weight: 600; }
.ann  { color: #ca8a04; font-weight: 600; }
.exc  { color: #ef4444; font-weight: 700; }
.kw2  { color: #f97316; font-weight: 700; }

/* Info boxes */
.info-box {
  border-radius: var(--info-radius, 10px);
  padding: 16px 18px;
  font-size: 14.5px;
  line-height: 1.6;
  font-family: var(--info-font, inherit);
  border-style: solid;
  border-top-width:    var(--info-bd-t-w, 1px);
  border-right-width:  var(--info-bd-r-w, 1px);
  border-bottom-width: var(--info-bd-b-w, 1px);
  border-left-width:   var(--info-bd-l-w, 1px);
  margin: var(--info-mt, 0) var(--info-mr, 0) var(--info-mb, 20px) var(--info-ml, 0);
}
.info-box strong { font-weight: 700; }
.info-box.tip,
.info-box.key { border-color: rgba(var(--accent-rgb),0.28); background: rgba(var(--accent-rgb),0.06); color: var(--text2); }
.info-box.warn { border-color: rgba(var(--warn-rgb),0.28); background: rgba(var(--warn-rgb),0.07); color: var(--text2); }
.info-box.good { border-color: rgba(var(--ok-rgb),0.28); background: rgba(var(--ok-rgb),0.06); color: var(--text2); }
.info-box.bad,
.info-box.err  { border-color: rgba(var(--err-rgb),0.35);  background: rgba(var(--err-rgb),0.07);  color: var(--text2); }

/* Buttons */
.btn {
  font-family: var(--btn-font, var(--font-mono, 'Consolas', 'Monaco', 'Courier New', monospace));
  font-size: 13.5px;
  padding: 10px 20px;
  border-radius: var(--btn-radius, 8px);
  cursor: pointer;
  border-top:    var(--btn-bd-t-w, 1px) solid var(--btn-bd-t-c, var(--border));
  border-right:  var(--btn-bd-r-w, 1px) solid var(--btn-bd-r-c, var(--border));
  border-bottom: var(--btn-bd-b-w, 1px) solid var(--btn-bd-b-c, var(--border));
  border-left:   var(--btn-bd-l-w, 1px) solid var(--btn-bd-l-c, var(--border));
  background: var(--btn-bg, var(--card));
  color: var(--btn-fg, var(--text));
  margin: var(--btn-mt, 0) var(--btn-mr, 0) var(--btn-mb, 0) var(--btn-ml, 0);
  transition: all 0.2s;
  letter-spacing: 0.3px;
  font-weight: 500;
}
.btn:hover { border-color: rgba(var(--accent-rgb), 0.45); background: var(--card); }
.btn.primary { background: var(--accent); color: white; border-color: var(--accent); }
.btn.primary:hover { filter: brightness(0.92); }

/* Quiz — score panel */
.score-display {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 24px 32px;
  margin-bottom: 28px;
  display: flex;
  gap: 40px;
  align-items: center;
  flex-wrap: wrap;
  box-shadow: 0 1px 2px rgba(15,20,32,.03);
}
.score-label { font-family: var(--font-mono, 'Consolas', monospace); font-size: 11px; letter-spacing: 0.16em; color: var(--text3); text-transform: uppercase; margin-bottom: 4px; }
.score-value { font-size: 2rem; font-weight: 700; color: var(--accent); font-family: var(--font-mono, 'Consolas', monospace); }
.quiz-dots { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.quiz-dot  { width: 14px; height: 14px; border-radius: 50%; background: var(--border); transition: background 0.3s; }
.quiz-dot.correct { background: var(--ok); }
.quiz-dot.wrong   { background: var(--err); }

/* Quiz — questions and options */
.quiz-q {
  background: var(--quiz-bg, var(--card));
  color: var(--quiz-fg, inherit);
  font-family: var(--quiz-font, inherit);
  border-top:    var(--quiz-bd-t-w, 1px) solid var(--quiz-bd-t-c, var(--border));
  border-right:  var(--quiz-bd-r-w, 1px) solid var(--quiz-bd-r-c, var(--border));
  border-bottom: var(--quiz-bd-b-w, 1px) solid var(--quiz-bd-b-c, var(--border));
  border-left:   var(--quiz-bd-l-w, 1px) solid var(--quiz-bd-l-c, var(--border));
  border-radius: var(--quiz-radius, 10px);
  padding: 26px;
  margin: var(--quiz-mt, 0) var(--quiz-mr, 0) var(--quiz-mb, 20px) var(--quiz-ml, 0);
  box-shadow: 0 1px 2px rgba(15,20,32,.03), 0 20px 46px -34px rgba(15,20,32,.3);
}
.quiz-q .q-text { font-weight: 600; font-size: 1.1em; margin-bottom: 20px; line-height: 1.7; color: var(--text); }
.quiz-q .q-text code { font-family: var(--font-mono, 'Consolas', monospace); font-size: 0.9em; background: var(--bg2); border: 1px solid var(--border); padding: 2px 6px; border-radius: 5px; color: var(--accent); }

.quiz-options { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 640px) { body:not(.slide-layout) .quiz-options { grid-template-columns: 1fr; } }

.quiz-opt {
  background: var(--qopt-bg, var(--bg2));
  border-top:    var(--qopt-bd-t-w, 1px) solid var(--qopt-bd-t-c, var(--border));
  border-right:  var(--qopt-bd-r-w, 1px) solid var(--qopt-bd-r-c, var(--border));
  border-bottom: var(--qopt-bd-b-w, 1px) solid var(--qopt-bd-b-c, var(--border));
  border-left:   var(--qopt-bd-l-w, 1px) solid var(--qopt-bd-l-c, var(--border));
  border-radius: var(--qopt-radius, 8px);
  padding: 14px 18px;
  margin: var(--qopt-mt, 0) var(--qopt-mr, 0) var(--qopt-mb, 0) var(--qopt-ml, 0);
  text-align: start;
  cursor: pointer;
  font-size: 1em;
  transition: all 0.2s;
  font-family: var(--qopt-font, inherit);
  color: var(--qopt-fg, var(--text));
  line-height: 1.5;
}
.quiz-opt:hover:not(:disabled):not(.disabled) { border-color: var(--accent); background: rgba(var(--accent-rgb),0.06); }
.quiz-opt.correct  { border-color: var(--ok); background: rgba(var(--ok-rgb),0.12); color: var(--text); }
.quiz-opt.wrong    { border-color: var(--err); background: rgba(var(--err-rgb),0.12);  color: var(--text); animation: shake 0.5s ease; }
.quiz-opt:disabled, .quiz-opt.disabled { cursor: default; opacity: 0.85; }

.q-feedback {
  display: none;
  margin-top: 16px;
  padding: 14px 18px;
  background: rgba(var(--ok-rgb),0.08);
  border: 1px solid rgba(var(--ok-rgb),0.3);
  border-radius: 8px;
  font-size: 0.95rem;
  color: var(--text2);
  line-height: 1.7;
}

/* Dynamic-chapter flow layout (emitted by _dynamicChapterRender.js only —
   static chapters never produce .blocks-flow). One flex item per block;
   full-width blocks stack exactly as normal flow, while blocks carrying a
   widthPct/align inline style can share a row with their neighbors. */
.blocks-flow { display: flex; flex-wrap: wrap; align-items: flex-start; }
.blocks-flow > * { width: 100%; min-width: 0; }
@media (max-width: 768px) {
  /* Stack side-by-side blocks on narrow screens. NOT on a frozen slide
     canvas — there the column width is fixed, so this `!important` would
     silently re-flow a rehearsed slide on a small screen. */
  body:not(.slide-layout) .blocks-flow > * { width: 100% !important; margin-inline: 0 !important; }
}

/* ── Content cards (course-page design 2026-07-18) ──────────────
   Block groups (read mode AND editor canvas since the 2026-07-18
   unification): the renderer wraps each h2/h3-led run of .blk leaves in
   a .content-card, so ALL lesson content sits on white kicker-led panels
   exactly like the mockup. Structure contract:
     .blocks-flow > .content-card > .blk
   The card itself is the flex-wrap row now, so per-block widthPct/align
   side-by-side layout keeps working INSIDE a card. Editable leaves carry
   `blk ax-block` so these rules style the editor canvas identically. */
.content-card {
  width: 100%;
  background: var(--card-bg, var(--card));
  color: var(--card-fg, inherit);
  font-family: var(--card-font, inherit);
  border-top:    var(--card-bd-t-w, 1px) solid var(--card-bd-t-c, var(--border));
  border-right:  var(--card-bd-r-w, 1px) solid var(--card-bd-r-c, var(--border));
  border-bottom: var(--card-bd-b-w, 1px) solid var(--card-bd-b-c, var(--border));
  border-left:   var(--card-bd-l-w, 1px) solid var(--card-bd-l-c, var(--border));
  border-radius: var(--card-radius, 10px);
  padding: 26px;
  margin: var(--card-mt, 28px) var(--card-mr, 0) var(--card-mb, 0) var(--card-ml, 0);
  box-shadow: 0 1px 2px rgba(15,20,32,.03), 0 20px 46px -34px rgba(15,20,32,.3);
  scroll-margin-top: 80px;
  display: flex; flex-wrap: wrap; align-items: flex-start;
}
.content-card:first-child { margin-top: var(--card-mt, 32px); }
.content-card > .blk { width: 100%; min-width: 0; }
/* Sized side-by-side flow panels wrap on their OWN width, not the viewport: a
   floor of min(100%, 22rem) makes the second panel drop below when the card
   can't hold two, while min(100%,…) keeps a lone narrow card at one column. */
.content-card > .blk[style*="width"],
.blocks-flow  > .blk[style*="width"] { min-width: min(100%, 22rem); }
@media (max-width: 768px) {
  body:not(.slide-layout) .content-card > .blk { width: 100% !important; margin-inline: 0 !important; }
}
/* The card-leading heading renders as the mono uppercase kicker (it stays
   a real h2/h3 so the right-rail TOC and Present-mode heading splits keep
   working). Inner h4s — and h2/h3s that are not card-leading (only
   possible in legacy/foreign markup) — keep normal heading styling. */
.content-card > .blk:first-child > h2:first-child,
.content-card > .blk:first-child > h3:first-child {
  font-family: var(--font-mono, 'Consolas', 'Monaco', 'Courier New', monospace);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text3);
  font-weight: 500;
  line-height: 1.5;
  margin: 0 0 20px;
}
/* Nested `card` blocks become the mockup's inner mini-cards. */
.content-card .card {
  border-top:    var(--mcard-bd-t-w, 1px) solid var(--mcard-bd-t-c, var(--border));
  border-right:  var(--mcard-bd-r-w, 1px) solid var(--mcard-bd-r-c, var(--border));
  border-bottom: var(--mcard-bd-b-w, 1px) solid var(--mcard-bd-b-c, var(--border));
  border-left:   var(--mcard-bd-l-w, 1px) solid var(--mcard-bd-l-c, var(--border));
  border-radius: var(--mcard-radius, 10px);
  padding: 18px;
  margin: var(--mcard-mt, 0) var(--mcard-mr, 0) var(--mcard-mb, 18px) var(--mcard-ml, 0);
  color: var(--mcard-fg, inherit);
  font-family: var(--mcard-font, inherit);
  box-shadow: none;
}
.content-card .card .card-title {
  font-family: var(--font-body, inherit);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
  margin-bottom: 14px;
}
/* Trim trailing gaps so card padding is the only bottom spacing. */
.content-card > .blk:last-child > :last-child { margin-bottom: 0; }
/* Presenting: cards dissolve entirely — slides measure/toggle the .blk
   leaves exactly like the pre-carding flat DOM (see _presentMode.js
   sectionUnits). display:contents removes the box, padding and border. */
body.presenting .content-card { display: contents; }

/* Cross-chapter links footer block */
.cross-links {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 32px;
  padding-top: 24px;
  border-top: 1px solid var(--border);
}
.cross-links a {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 14px;
  color: var(--accent);
  text-decoration: none;
  padding: 8px 16px;
  border: 1px solid rgba(var(--accent-rgb),0.3);
  border-radius: 6px;
  transition: all 0.2s;
}
.cross-links a:hover { background: rgba(var(--accent-rgb),0.08); border-color: var(--accent); }

footer {
  text-align: center;
  padding: 24px;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 13px;
  color: var(--text3);
  border-top: 1px solid var(--border);
  position: relative;
  z-index: 1;
}

/* Font-size toolbar (rendered by font-size.js) */
#fontToolbar {
  position: fixed; bottom: 20px; inset-inline-end: 20px;
  background: var(--card); border: 1px solid var(--border);
  border-radius: 10px; padding: 8px 14px;
  display: flex; gap: 8px; align-items: center;
  z-index: 200; box-shadow: 0 2px 12px rgba(15,20,32,0.08);
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 13px;
}
#fontToolbar button {
  background: var(--bg2); border: 1px solid var(--border);
  border-radius: 6px; padding: 4px 10px; cursor: pointer; font-weight: 700;
}
#fontToolbar button:hover { border-color: var(--accent); }
.ft-label { font-weight: 700; color: var(--text2); margin-inline-end: 4px; }

/* ════════════════════════════════════════════════════════════
   Shared education widgets — promoted from per-page inline
   <style> blocks (2026-07-10) so every surface shares one
   definition. A page's own inline <style> (linked after this
   file) may still override any of these.
   ════════════════════════════════════════════════════════════ */

/* Topic grid — Overview shortcut cards (chapters, labs, guide) */
.topic-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 28px; }
.topic-card { background: var(--topic-bg, var(--card)); color: var(--topic-fg, inherit); font-family: var(--topic-font, inherit); border-top: var(--topic-bd-t-w, 1px) solid var(--topic-bd-t-c, var(--border)); border-right: var(--topic-bd-r-w, 1px) solid var(--topic-bd-r-c, var(--border)); border-bottom: var(--topic-bd-b-w, 1px) solid var(--topic-bd-b-c, var(--border)); border-left: var(--topic-bd-l-w, 1px) solid var(--topic-bd-l-c, var(--border)); border-radius: var(--topic-radius, 10px); padding: 22px; margin: var(--topic-mt, 0) var(--topic-mr, 0) var(--topic-mb, 0) var(--topic-ml, 0); cursor: pointer; transition: all .2s; box-shadow: 0 1px 2px rgba(15,20,32,.03); }
.topic-card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(var(--accent-rgb), .15); }
/* Topic-card icon: canonical outline SVG (shared/icons.js via AcademeXIcons.resolve),
   accent-tinted. font-size kept as a fallback for any legacy text glyph. */
.topic-card .tc-icon { font-size: 1.8rem; line-height: 0; margin-bottom: 12px; color: var(--accent); }
.topic-card .tc-icon svg { width: 1.8rem; height: 1.8rem; display: block; }
.topic-card .tc-name { font-weight: 700; font-size: 1.1rem; margin-bottom: 8px; }
.topic-card .tc-desc { font-size: .9rem; color: var(--text2); line-height: 1.6; }

/* Try-It accordion — collapsible puzzle / common-mistake panel */
.try-it { border: 1px solid rgba(var(--accent-rgb), .3); border-radius: 10px; margin: 28px 0; background: rgba(var(--accent-rgb), .03); overflow: hidden; }
.try-it-header { padding: 14px 20px; cursor: pointer; display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 1rem; color: var(--accent); user-select: none; }
.try-it-header:hover { background: rgba(var(--accent-rgb), .06); }
.try-it-header .arrow { transition: transform .3s; font-size: .8em; }
.try-it.open .try-it-header .arrow { transform: rotate(90deg); }
.try-it-body { max-height: 0; overflow: hidden; transition: max-height .4s ease; padding: 0 20px; }
.try-it.open .try-it-body { max-height: 1200px; padding: 0 20px 20px; }
.try-it-body p { font-size: .95rem; margin-bottom: 12px; }
.try-it-body .code-block { padding: var(--code-padding-compact, 12px); margin-bottom: 12px; }
.try-it-body .expected { font-family: var(--font-mono, 'Consolas', monospace); font-size: 13px; background: var(--bg3); padding: 12px; border-radius: 6px; color: var(--text2); white-space: pre-wrap; }

/* Console — dark terminal-style output area for widgets/tracers */
.console { background: #1e293b; border-radius: 8px; padding: 20px 24px; font-family: var(--font-mono, 'Consolas', monospace); font-size: 14px; color: #a5f3fc; line-height: 1.8; margin-bottom: 24px; min-height: 52px; border: 1px solid #334155; }
.console .prompt { color: #4ade80; }
.console .line { display: block; }
.console .line.err { color: #f87171; }

/* Labelled side-by-side code comparison. Auto-wrapping tracks collapse to one
   column on the CONTAINER's own width, so columns stack inside the narrowed
   reading area rather than only at a fixed viewport width. */
.side-by-side { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 20px; margin-bottom: 24px; }
/* min-width:0 on every column stops wide children (code/tables) from blowing
   out the grid track and keeping two columns "fitting". Covers both the
   renderer's `.sbs-col` wrappers AND hand-authored `html` blocks whose columns
   are bare <div>s (e.g. the migrated ch1 Arrays runnable loop comparison). */
.side-by-side > * { min-width: 0; }

/* `illustration` block (2026-07-18): AI-or-hand-authored self-contained
   visual (inline SVG/CSS/JS). The figure gives it consistent chrome; wide
   diagrams scroll inside it instead of breaking the lesson column. */
.illustration-block { margin: 0 0 20px; overflow-x: auto; max-width: 100%; }
.illustration-block svg { max-width: 100%; height: auto; }
.illus-title { font-family: var(--font-mono, 'Consolas', monospace); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 10px; font-weight: 700; color: var(--text2); }
/* Backstop for generated animation that ignores the prompt-level rule. */
@media (prefers-reduced-motion: reduce) {
  .illustration-block * { animation: none !important; transition: none !important; }
}
.side-label { font-family: var(--font-mono, 'Consolas', monospace); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 10px; font-weight: 700; color: var(--text2); }

/* ── Visualization widgets (`viz-*`, 2026-08-05) ────────────────
   The standard chrome for an interactive chapter visualizer: a framed
   stage to draw in, a narration line that updates as the widget steps,
   labelled controls, selectable chips, array cells, and the SVG figure
   primitives hand-authored diagrams are built from.

   The widget's LOGIC stays page-local (a chapter's `pageJs` + an `html`
   block) — only its appearance is standardized here, so every course
   reads as one system instead of eleven forks. Every colour comes from a
   token, so a visualizer inherits the course accent for free. Buttons use
   the canonical `.btn` / `.btn-group`; logs use `.console`; verdicts use
   `.feedback`. See docs/design-system.md § Visualization widgets. */

/* Stage — the framed surface a visualizer draws into. */
.viz-stage { background: var(--bg2); border: 1px solid var(--border); border-radius: 12px; padding: 14px; margin: 14px 0; }
.viz-stage--flush { padding: 0; overflow: hidden; }
.viz-stage svg { width: 100%; height: auto; display: block; }

/* Narration — the line a widget rewrites as its state changes. */
.viz-note { font-size: .92rem; color: var(--text2); line-height: 1.65; margin-top: 8px; min-height: 2.4em; }
.viz-note b, .viz-note strong { color: var(--text); }

/* Legend — colour key for a stage. */
.viz-legend { display: flex; flex-wrap: wrap; gap: 14px; margin: 10px 0 4px; font-size: .82rem; color: var(--text3); }
.viz-key { display: inline-flex; align-items: center; gap: 6px; }
.viz-key-dot { width: 11px; height: 11px; border-radius: 3px; background: var(--accent); flex-shrink: 0; }

/* Controls — labelled sliders, selects and number inputs. */
.viz-ctl { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 12px 0; }
.viz-ctl label { font-size: .85rem; color: var(--text2); font-family: var(--font-mono, 'Consolas', monospace); }
.viz-ctl--stack label { display: flex; flex-direction: column; gap: 4px; font-size: .74rem; color: var(--text3); text-transform: uppercase; letter-spacing: .06em; }
.viz-range { flex: 1 1 180px; min-width: 140px; accent-color: var(--accent); }
.viz-out { font-family: var(--font-mono, 'Consolas', monospace); font-weight: 700; color: var(--accent); min-width: 3ch; }
.viz-select, .viz-num { padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--text); font-family: var(--font-body, inherit); font-size: .9rem; }
.viz-num { width: 76px; }

/* Chips — selectable tokens (classify / pick-one widgets). */
.viz-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; min-height: 42px; }
.viz-chip { padding: 8px 14px; border: 1.5px solid var(--border); border-radius: 999px; background: var(--card); color: var(--text); font-family: var(--font-body, inherit); font-size: .9rem; cursor: pointer; transition: all .18s; }
.viz-chip:hover { border-color: var(--accent); }
.viz-chip.sel { border-color: var(--accent); background: rgba(var(--accent-rgb), .12); font-weight: 600; }
.viz-chip.done { border-color: var(--ok); background: rgba(var(--ok-rgb), .14); cursor: default; }

/* Drop targets — buckets a chip is sorted into. */
.viz-buckets { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
.viz-bucket { border: 1.5px dashed var(--border); border-radius: 12px; padding: 12px; min-height: 110px; background: var(--bg2); transition: all .18s; }
.viz-bucket.armed { border-color: var(--accent); cursor: pointer; }
.viz-bucket.bad { border-color: var(--err); animation: viz-shake .35s; }
.viz-bucket-h { font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text3); margin-bottom: 8px; font-weight: 700; }
.viz-bucket-body { display: flex; flex-wrap: wrap; gap: 6px; }

/* Cells — the array/slot primitive every linear structure is drawn from.
   `.live` = inside the structure, `.tip` = the head/top/rear marker,
   `.mark` = the element under inspection, `.bad` = an illegal access. */
.viz-cells { display: flex; gap: 4px; flex-wrap: wrap; margin: 14px 0 8px; }
.viz-cell { width: 38px; height: 44px; border: 1.5px solid var(--border); border-radius: 6px; background: var(--card); display: flex; align-items: center; justify-content: center; font-family: var(--font-mono, 'Consolas', monospace); font-size: 1rem; color: var(--text); transition: all .3s; position: relative; }
.viz-cell.live { border-color: var(--accent); background: rgba(var(--accent-rgb), .10); }
.viz-cell.tip { border-color: var(--ok); background: rgba(var(--ok-rgb), .16); }
.viz-cell.mark { border-color: var(--warn); background: rgba(var(--warn-rgb), .16); }
.viz-cell.bad { border-color: var(--err); background: rgba(var(--err-rgb), .14); }
.viz-cell.flash { animation: viz-pop .3s ease-out; }
.viz-cell-i { position: absolute; bottom: -16px; font-size: .62rem; color: var(--text4); }
.viz-cells--indexed { margin-bottom: 22px; }

/* Boxes — a labelled value/record/node drawn in flow (not in SVG). */
.viz-box { border: 2px solid var(--border); border-radius: 10px; background: var(--card); padding: 10px 14px; text-align: center; transition: all .35s; }
.viz-box.lead { border-color: var(--accent); background: rgba(var(--accent-rgb), .08); }
.viz-box-k { display: block; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text3); margin-bottom: 4px; }
.viz-box-v { font-family: var(--font-mono, 'Consolas', monospace); font-size: .95rem; color: var(--text); }
.viz-atom { display: inline-flex; width: 30px; height: 34px; align-items: center; justify-content: center; border: 1px solid var(--border); border-radius: 6px; background: var(--bg3); font-family: var(--font-mono, 'Consolas', monospace); font-size: .9rem; margin: 0 2px; animation: viz-pop .3s ease-out; }

/* SVG figure primitives — hand-authored structural diagrams and charts.
   Wrap the <svg> in `.viz-fig`; children use the `fig-*` classes so a
   figure recolours with the theme instead of carrying literal hex. */
.viz-fig { width: 100%; max-width: 680px; height: auto; display: block; margin: 0 auto; }
.viz-fig .fig-box rect { fill: var(--bg3); stroke: var(--border); stroke-width: 1.5; }
.viz-fig .fig-box text { fill: var(--text); font-family: var(--font-body, inherit); font-size: 15px; font-weight: 600; text-anchor: middle; }
.viz-fig .fig-box .fig-sub { fill: var(--text3); font-size: 12px; font-weight: 400; }
.viz-fig .fig-accent rect { fill: rgba(var(--accent-rgb), .10); stroke: var(--accent); }
.viz-fig .fig-ok rect { fill: rgba(var(--ok-rgb), .12); stroke: var(--ok); }
.viz-fig .fig-warn rect { fill: rgba(var(--warn-rgb), .12); stroke: var(--warn); }
.viz-fig .fig-soft rect { fill: var(--bg2); }
.viz-fig .fig-leaf rect { fill: var(--card); }
.viz-fig .fig-leaf text { font-size: 13px; font-weight: 500; }
.viz-fig .fig-link { fill: none; stroke: var(--text3); stroke-width: 1.4; }
.viz-fig .fig-arrow { stroke: var(--text3); stroke-width: 1.6; fill: none; }
.viz-fig .fig-axis { stroke: var(--text3); stroke-width: 1.5; }
.viz-fig .fig-grid { stroke: var(--border); stroke-width: 1; }
.viz-fig .fig-gtxt { fill: var(--text4); font-size: 10px; font-family: var(--font-mono, 'Consolas', monospace); }
.viz-fig .fig-ax-lbl { fill: var(--text3); font-size: 12px; text-anchor: middle; font-family: var(--font-body, inherit); }
.viz-fig .fig-cap { fill: var(--text2); font-size: 12.5px; text-anchor: middle; font-family: var(--font-body, inherit); }
.viz-fig .fig-em { fill: var(--accent); font-weight: 700; }
.viz-fig .fig-cut { stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 5 4; }
.viz-fig .fig-shade { fill: rgba(var(--accent-rgb), .07); }
.viz-fig .fig-shade-bad { fill: rgba(var(--err-rgb), .14); }
.viz-fig .fig-curve { fill: none; stroke-width: 2.4; stroke: var(--accent); }
.viz-fig .fig-c-ok { stroke: var(--ok); } .viz-fig .fig-c-warn { stroke: var(--warn); } .viz-fig .fig-c-err { stroke: var(--err); }
.viz-fig .fig-t-ok { fill: var(--ok); } .viz-fig .fig-t-warn { fill: var(--warn); } .viz-fig .fig-t-err { fill: var(--err); }
.viz-fig .fig-lbl { font-size: 12px; font-family: var(--font-body, inherit); fill: var(--text2); }
/* Node + edge — the graph/tree primitive (an SVG <g> that may be moved
   with a transform, so it animates between layouts). */
.viz-fig .fig-node, .viz-stage .fig-node { transition: transform .55s cubic-bezier(.4,0,.2,1); }
.viz-fig .fig-node circle, .viz-stage .fig-node circle { fill: var(--card); stroke: var(--accent); stroke-width: 2; }
.viz-fig .fig-node text, .viz-stage .fig-node text { fill: var(--text); font-family: var(--font-mono, 'Consolas', monospace); font-size: 15px; font-weight: 700; text-anchor: middle; dominant-baseline: central; }
.viz-fig .fig-edge, .viz-stage .fig-edge { stroke: rgba(var(--accent-rgb), .55); stroke-width: 2; fill: none; transition: opacity .4s; }

/* ── Equations (`eqn-*`, 2026-08-05) ────────────────────────────
   Display and inline mathematics for the engineering courses. The platform
   ships no math library and adds no CDN — an equation is plain HTML styled
   from the same tokens as everything else, so it stays selectable,
   searchable and screen-readable, follows the reader's font size, and
   recolours with the course accent. Only the LAYOUT is CSS.

   Math that belongs inside a figure goes in that SVG's own <text>, not here.

   Display equation with a stacked fraction and an equation number:

     <div class="eqn">
       <span class="eqn-lbl">(5.14)</span>
       &tau;<sub>ind</sub> =
       <span class="eqn-frac">
         <span class="eqn-num">3 V<sub>TH</sub><sup>2</sup> (R<sub>2</sub>/s)</span>
         <span class="eqn-den">&omega;<sub>sync</sub>[(R<sub>TH</sub> + R<sub>2</sub>/s)<sup>2</sup>
                               + (X<sub>TH</sub> + X<sub>2</sub>)<sup>2</sup>]</span>
       </span>
     </div>
     <ul class="eqn-where">
       <li><b>&omega;<sub>sync</sub></b> synchronous speed, rad/s</li>
       <li><b>s</b> slip, dimensionless</li>
     </ul>

   A multi-line derivation aligns on the operator — `.eqn--steps` expects
   LHS / operator / RHS triples:

     <div class="eqn eqn--steps">
       <span class="eqn-lhs">P<sub>AG</sub></span><span class="eqn-op">=</span><span>3 I<sub>2</sub><sup>2</sup> R<sub>2</sub> / s</span>
       <span class="eqn-lhs">P<sub>conv</sub></span><span class="eqn-op">=</span><span>(1 &minus; s) P<sub>AG</sub></span>
     </div>

   Children are inline-level, so a long equation wraps around its fractions
   instead of clipping; a single oversized fraction scrolls. */

.eqn {
  position: relative;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 1.02rem; line-height: 1.5; color: var(--text);
  text-align: center;
  background: var(--bg2); border: 1px solid var(--border); border-radius: 10px;
  padding: 16px 18px; margin: 16px 0;
  overflow-x: auto;
}
/* Heavy sub/superscripts are the norm here (V_TH, I_A, n_sync, X_d), so they
   are tuned once rather than fought per chapter. `line-height:0` keeps a
   subscripted line the same height as an unsubscripted one. */
.eqn sub, .eqn sup { font-size: .72em; line-height: 0; font-style: normal; }
.eqn sub { vertical-align: -.3em; }
.eqn sup { vertical-align: .52em; }

/* Run-in variant — an equation inside a sentence, no card. */
.eqn--inline {
  display: inline-block; background: none; border: 0; border-radius: 0;
  padding: 0; margin: 0; font-size: 1em; text-align: left; overflow: visible;
}

/* Stacked fraction. The grid sizes the rule to the wider of the two rows. */
.eqn-frac {
  display: inline-grid; grid-template-rows: auto auto;
  justify-items: center; text-align: center;
  vertical-align: middle; margin: 0 .2em;
}
.eqn-num { padding: 0 .5em .14em; }
.eqn-den { padding: .16em .5em 0; border-top: 1.5px solid currentColor; }
.eqn-frac .eqn-frac { font-size: .92em; }

/* Phasor / vector overbar — V&#772; I&#772;. The border sits above the padding,
   so the padding is the gap between rule and glyph. */
.eqn-bar { display: inline-block; border-top: 1.3px solid currentColor; padding-top: .1em; }

/* Radical: <span class="eqn-sqrt"><span>b<sup>2</sup> &minus; 4ac</span></span> */
.eqn-sqrt::before { content: '\221A'; margin-right: .04em; }
.eqn-sqrt > span { display: inline-block; border-top: 1.3px solid currentColor; padding-top: .1em; }

/* Call one term out of an expression — "this term is the rotor copper loss".
   Variants are modifiers, never separate components. */
.eqn-hi { background: rgba(var(--accent-rgb), .14); border-radius: 4px; padding: .1em .3em; }
.eqn-hi--ok   { background: rgba(var(--ok-rgb), .16); }
.eqn-hi--warn { background: rgba(var(--warn-rgb), .16); }
.eqn-hi--err  { background: rgba(var(--err-rgb), .16); }

/* Equation number. Place it as the FIRST child of `.eqn`. */
.eqn-lbl { float: right; margin-left: 1em; font-family: var(--font-body, inherit); font-size: .78rem; color: var(--text3); }

/* Aligned derivation. Three children per row: LHS, operator, RHS. */
.eqn--steps {
  display: grid; grid-template-columns: max-content max-content 1fr;
  justify-content: center; align-items: center;
  row-gap: .55em; column-gap: .45em; text-align: left;
}
.eqn--steps .eqn-lhs { text-align: right; color: var(--text); }
.eqn--steps .eqn-op  { color: var(--text3); }

/* The "where …" symbol table that follows an equation. */
.eqn-where {
  list-style: none; margin: -8px 0 18px; padding: 0 4px;
  font-family: var(--font-body, inherit); font-size: .86rem; color: var(--text2); line-height: 1.7;
}
.eqn-where li { display: flex; gap: .6em; align-items: baseline; }
.eqn-where li > b, .eqn-where li > strong {
  flex: 0 0 auto; min-width: 5.5em;
  font-family: var(--font-mono, 'Consolas', monospace); font-weight: 600; color: var(--text);
}

/* ── `simulation` block player (2026-08-05) ─────────────────────
   Chrome for the data-only step-through block rendered by
   _dynamicChapterRender.js `renderSimulation`. Everything inside the stage is
   drawn with the viz-* components above; only the player's own furniture —
   the stack column, the input tape, the output line and the progress bar —
   lives here. Declared in CSS (never injected by the player) so a re-render
   can never duplicate a <style> tag. */
.sim { border: 1px solid var(--border); border-radius: 12px; background: var(--card); padding: 20px 22px; margin-bottom: 24px; }
.sim > .card-title { margin-bottom: 8px; }
.sim-intro { font-size: .95rem; color: var(--text2); line-height: 1.7; margin-bottom: 14px; }
.sim-svg { width: 100%; height: auto; display: block; }
.sim-empty { color: var(--text4); font-family: var(--font-mono, 'Consolas', monospace); font-size: .85rem; text-align: center; padding: 22px 0; }

/* stack column — newest on top, anchored to the bottom of the stage */
.sim-stack { display: flex; flex-direction: column-reverse; justify-content: flex-start; gap: 4px; min-height: 120px; }
.sim-item { border: 1.5px solid var(--accent); border-radius: 6px; background: rgba(var(--accent-rgb), .10); padding: 7px 12px; font-family: var(--font-mono, 'Consolas', monospace); font-size: .9rem; color: var(--text); text-align: center; animation: viz-pop .25s ease-out; }
.sim-item.top { border-color: var(--ok); background: rgba(var(--ok-rgb), .16); font-weight: 700; }

/* input tape — the token sequence being consumed, with a cursor */
.sim-tape { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 12px; }
.sim-tok { border: 1px solid var(--border); border-radius: 5px; background: var(--card); padding: 5px 9px; font-family: var(--font-mono, 'Consolas', monospace); font-size: .86rem; color: var(--text3); transition: all .2s; }
.sim-tok.done { opacity: .5; color: var(--text4); }
.sim-tok.now { border-color: var(--accent); background: rgba(var(--accent-rgb), .14); color: var(--text); font-weight: 700; }

.sim-out { font-family: var(--font-mono, 'Consolas', monospace); font-size: .95rem; letter-spacing: .06em; color: var(--text); background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; margin-top: 12px; }
.sim-code { white-space: pre; margin-top: 12px; }
.sim-code .now { background: rgba(var(--accent-rgb), .16); border-radius: 3px; }
.sim-stat { margin-top: 12px; margin-bottom: 0; }
.sim-body .viz-note { margin-top: 10px; }

/* progress rail — how far through the current script the player is */
.sim-progress { height: 3px; background: var(--bg3); border-radius: 2px; overflow: hidden; margin-top: 14px; }
.sim-progress > span { display: block; height: 100%; background: var(--accent); transition: width .3s ease; }

@media (prefers-reduced-motion: reduce) {
  .sim-item { animation: none; }
  .sim-tok, .sim-progress > span { transition: none; }
}

@keyframes viz-pop { from { transform: scale(.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes viz-shake { 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
@media (prefers-reduced-motion: reduce) {
  .viz-cell, .viz-box, .viz-chip, .viz-bucket, .fig-node, .fig-edge { transition: none; }
  .viz-atom, .viz-cell.flash, .viz-bucket.bad { animation: none; }
}
@media (max-width: 640px) {
  .viz-cell { width: 32px; height: 38px; font-size: .9rem; }
}

/* Button row */
.btn-group { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.btn:disabled, .btn[disabled] { opacity: .5; cursor: not-allowed; }

/* Stepper — sequential step walkthrough */
.stepper { display: flex; flex-direction: column; gap: 0; margin-bottom: 24px; }
.step-item { display: flex; gap: 16px; align-items: flex-start; padding: 14px 0; opacity: .3; transition: all .4s; border-bottom: 1px solid var(--border); }
.step-item:last-child { border-bottom: none; }
.step-item.active { opacity: 1; }
.step-item.done { opacity: .6; }
/* --static: authored `stepper` blocks (no page-JS state machine) render
   every step fully opaque — the base opacity ramp is widget styling. */
.stepper--static .step-item { opacity: 1; }
.step-num { width: 32px; height: 32px; border-radius: 50%; background: var(--bg2); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; font-family: var(--font-mono, 'Consolas', monospace); font-size: 13px; font-weight: 700; flex-shrink: 0; transition: all .3s; }
.step-item.active .step-num { background: var(--accent); color: #fff; border-color: var(--accent); }
.step-item.done .step-num { background: var(--ok); color: #fff; border-color: var(--ok); }
.step-content { flex: 1; }
.step-content .step-title { font-weight: 700; font-size: 1rem; margin-bottom: 4px; }
.step-content .step-desc { font-size: .9rem; color: var(--text2); line-height: 1.6; }

/* Education table — comparison / fact tables (truth tables,
   variables, assessments). Verdict cells use the badge classes. */
.io-table { width: 100%; border-collapse: collapse; font-size: 1em; margin: var(--table-mt, 0) var(--table-mr, 0) var(--table-mb, 20px) var(--table-ml, 0); }
.io-table th { font-family: var(--table-font, var(--font-mono, 'Consolas', monospace)); font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase; padding: 14px 18px; text-align: start; border-bottom: var(--table-bd-b-w, 2px) solid var(--table-bd-b-c, var(--border)); color: var(--text2); font-weight: 700; background: var(--bg2); }
.io-table td { padding: 14px 18px; border-bottom: var(--table-bd-b-w, 1px) solid var(--table-bd-b-c, var(--border)); vertical-align: middle; font-family: var(--table-font, var(--font-mono, 'Consolas', monospace)); font-size: 13.5px; color: var(--table-fg, inherit); }
.io-table tr:last-child td { border-bottom: none; }
.io-table tr:hover td { background: var(--bg2); }
.badge-true  { color: var(--ok); font-weight: 700; }
.badge-false { color: var(--err); font-weight: 700; }
.badge-new   { background: rgba(var(--ok-rgb),0.1); color: var(--ok); padding: 2px 10px; border-radius: 20px; font-size: 12px; font-weight: 700; }

/* Feedback box — unified success/error/hint result panel
   (Code Lab run results, practice checks, lab verify). Hidden
   while empty; JS toggles style.display or fills content. */
.feedback { border-radius: 8px; padding: 16px 20px; font-size: 0.95rem; line-height: 1.7; margin-bottom: 16px; }
.feedback:empty { display: none; }
.feedback.success { border: 1px solid rgba(var(--ok-rgb),0.3);   background: rgba(var(--ok-rgb),0.06);   color: var(--text2); }
.feedback.error   { border: 1px solid rgba(var(--err-rgb),0.3);  background: rgba(var(--err-rgb),0.06);  color: var(--text2); }
.feedback.hint    { border: 1px solid rgba(var(--warn-rgb),0.3); background: rgba(var(--warn-rgb),0.06); color: var(--text2); }

/* Chapter images (dynamic image block) + draft banner */
.chapter-image { display: block; max-width: 100%; height: auto; border-radius: 8px; margin: 20px auto; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
.chapter-image-cap { text-align: center; font-size: 0.85rem; color: var(--text3); margin-top: -10px; margin-bottom: 28px; font-style: italic; }
.draft-banner { background: #fef3c7; border-inline-start: 4px solid var(--warn); color: #92400e; padding: 12px 60px; font-size: 0.9rem; }

/* Hub cards — clickable navigation tiles (course hub; reusable).
   Children are scoped under .hub-card so they never collide with
   page-level .card-* classes. */
.hub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px; margin-bottom: 16px;
}
.hub-card {
  background: var(--hub-bg, var(--card));
  border-top:    var(--hub-bd-t-w, 1px) solid var(--hub-bd-t-c, var(--border));
  border-right:  var(--hub-bd-r-w, 1px) solid var(--hub-bd-r-c, var(--border));
  border-bottom: var(--hub-bd-b-w, 1px) solid var(--hub-bd-b-c, var(--border));
  border-left:   var(--hub-bd-l-w, 1px) solid var(--hub-bd-l-c, var(--border));
  border-radius: var(--hub-radius, 12px); padding: 28px; cursor: pointer;
  margin: var(--hub-mt, 0) var(--hub-mr, 0) var(--hub-mb, 0) var(--hub-ml, 0);
  transition: all 0.2s; position: relative; overflow: hidden;
  text-decoration: none; color: var(--hub-fg, inherit); font-family: var(--hub-font, inherit); display: block;
  box-shadow: 0 1px 2px rgba(15,20,32,.03), 0 20px 46px -34px rgba(15,20,32,.25);
}
.hub-card::before {
  content: ''; position: absolute; top: 0; inset-inline: 0; height: 3px;
  background: var(--card-color, var(--accent));
}
.hub-card:hover {
  transform: translateY(-3px); border-color: var(--card-color, var(--accent));
  box-shadow: 0 8px 24px color-mix(in srgb, var(--card-color, var(--accent)) 15%, transparent);
}
/* Card icon: outline SVG from the canonical set (shared/icons.js), tinted with
   the card's accent (--card-color → course accent for chapter cards). font-size
   is kept as a fallback for any legacy text glyph. */
.hub-card .card-icon { font-size: 2rem; line-height: 0; margin-bottom: 12px; color: var(--card-color, var(--accent)); }
.hub-card .card-icon svg { width: 2rem; height: 2rem; display: block; }
.hub-card .card-name { font-weight: 700; font-size: 1.2rem; margin-bottom: 6px; }
.hub-card .card-tag {
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 11px;
  letter-spacing: 0.08em; margin-bottom: 10px; font-weight: 600; text-transform: uppercase;
}
.hub-card .card-desc { font-size: 0.95rem; color: var(--text2); line-height: 1.6; margin-bottom: 12px; }
.hub-card .card-stats {
  display: flex; gap: 16px;
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 11.5px; color: var(--text3);
}
@media (max-width: 768px) {
  .hub-grid { grid-template-columns: 1fr; }
}

/* ════════════════════════════════════════════════════════════
   Course navigation chrome (2026-07-16) — named standard
   components rendered by courses/_shared/_courseNav.js around
   dynamic chapters, plus the hub's progress pill / resume chip.
   Sidebar (course tree), breadcrumbs, prev–next pager, mobile
   drawer. Tokens only — the course accent flows through
   --accent/--accent-rgb exactly like every other component.
   ════════════════════════════════════════════════════════════ */

/* Layout shell: single column by default; the sidebar grid engages
   only when _courseNav.js adds .has-nav (so previews, error states
   and static hosts keep the classic page). NO z-index here — a
   stacking context on the shell would trap the fixed mobile drawer
   (z-index 300) beneath the header (z-index 10).
   Grid: 288px course tree | lesson column | 248px on-this-page rail
   (rail ≥1280px only; sidebar ≥1024px, drawer below). */
.chapter-layout { position: relative; }
.chapter-body { min-width: 0; background: var(--lesson-content-bg, #f6f7f9); }
/* In-section headers are redundant under the chrome: the lesson header
   above <main> shows the active section's title/subtitle. (Present mode
   hides them too — it paginates around them; see body.presenting below.) */
.chapter-layout.has-nav .section .section-header { display: none; }
/* Column widths — three layers per side (weakest → strongest):
     stylesheet default (here) ← admin token (--course-nav-w /
     --lesson-rail-w, settings/theme via theme-loader) ← per-user drag
     (--course-nav-w-user / --lesson-rail-w-user, set by _courseNav.js
     bindColumnResizers(), persisted in localStorage
     'academex_lesson_cols'; double-click a handle to reset).
   The -user vars are deliberately OUTSIDE the theme whitelist so a
   theme revalidation can never clobber a personal drag. */
/* --nav-col / --rail-col: the resolved column widths (user drag → admin token →
   default), reused by the grid tracks, the full-height background stripes, and
   the divider handles. The background is painted as vertical column stripes on
   the LAYOUT (whose height is the whole page), so the sidebar/rail colours reach
   the end of the page — the sticky columns themselves are only as tall as their
   content and overlay their stripe in the same colour (seamless). */
@media (min-width: 1024px) {
  .chapter-layout.has-nav {
    --nav-col: var(--course-nav-w-user, var(--course-nav-w, 320px));
    display: grid; grid-template-columns: var(--nav-col) minmax(0, 1fr); align-items: start;
    background: linear-gradient(90deg,
      var(--course-nav-bg, #f8fafc) 0 var(--nav-col),
      var(--lesson-content-bg, #f6f7f9) var(--nav-col) 100%);
  }
  /* The lesson column is PINNED to track 2 rather than auto-placed. A
     display:none element is not a grid item, so a sidebar that is ever hidden
     (it was, briefly, while the outline loaded — 2026-08-05) leaves only two
     items for three tracks and auto-placement slides the whole lesson into the
     320px sidebar column. Pinned, a missing sidebar just leaves its track
     empty, painted by the shell's own background stripe. .rail-col stays
     auto-placed: it lands in track 3 after this one at ≥1280px, and there is
     no track 3 to pin it to between 1024–1279px. */
  .chapter-layout.has-nav > .chapter-body { grid-column: 2; }
}
@media (min-width: 1280px) {
  .chapter-layout.has-nav {
    --rail-col: var(--lesson-rail-w-user, var(--lesson-rail-w, 340px));
    grid-template-columns: var(--nav-col) minmax(0, 1fr) var(--rail-col);
    background: linear-gradient(90deg,
      var(--course-nav-bg, #f8fafc) 0 var(--nav-col),
      var(--lesson-content-bg, #f6f7f9) var(--nav-col) calc(100% - var(--rail-col)),
      var(--lesson-rail-bg, #f6f7f9) calc(100% - var(--rail-col)) 100%);
  }
}
/* Drag handles over the sidebar's right edge / rail's left edge. Children
   of .chapter-layout (the sticky columns clip absolute children via their
   overflow-y:auto), positioned at the grid lines via the width vars, so
   they track the columns while dragging. Invisible until hover/drag. */
.col-resizer {
  position: absolute; top: 0; bottom: 0; width: 10px;
  cursor: col-resize; z-index: 20; display: none;
  touch-action: none;
}
/* The line is the column divider (top:0 on the handle, a child of the
   relatively-positioned .chapter-layout, so its FULL extent is the whole page).
   Each side maps the admin tokens onto local --dv-* props the shared ::after
   reads: colour, thickness (--dv-w px) and vertical extent (--dv-len %, off the
   full-page handle). At rest it shows the divider colour; hover/drag brightens
   to accent. This replaces the sticky columns' own short border-right/left. */
.col-resizer--nav  { inset-inline-start: calc(var(--course-nav-w-user, var(--course-nav-w, 320px)) - 5px);
  --dv-color: var(--course-nav-divider, var(--border)); --dv-w: var(--course-nav-divider-w, 1px); --dv-len: var(--course-nav-divider-len, 100%); }
.col-resizer--rail { inset-inline-end: calc(var(--lesson-rail-w-user, var(--lesson-rail-w, 340px)) - 5px);
  --dv-color: var(--lesson-rail-divider, var(--border)); --dv-w: var(--lesson-rail-divider-w, 1px); --dv-len: var(--lesson-rail-divider-len, 100%); }
.col-resizer::after {
  content: ''; position: absolute; top: 0; bottom: auto;
  inset-inline-start: 4px; width: var(--dv-w, 1px); height: var(--dv-len, 100%);
  background: var(--dv-color, var(--border)); transition: background 0.15s, width 0.15s;
}
.col-resizer:hover::after,
.col-resizer.dragging::after { background: rgba(var(--accent-rgb), 0.45); width: max(2px, var(--dv-w, 1px)); border-radius: 2px; }
@media (min-width: 1024px) { .chapter-layout.has-nav > .col-resizer--nav { display: block; } }
@media (min-width: 1280px) { .chapter-layout.has-nav > .col-resizer--rail { display: block; } }
body.col-resizing { cursor: col-resize; user-select: none; }
body.col-resizing .col-resizer::after { background: rgba(var(--accent-rgb), 0.45); width: max(2px, var(--dv-w, 1px)); border-radius: 2px; }
/* Lesson column alignment when the chrome is active: header, main and
   pager share one measure inside the middle grid column. Like the mockup,
   content fills the column (40px gutters); the cap — admin-themable via
   the --lesson-max-w token — only tames ultra-wide monitors. */
.chapter-layout.has-nav .lesson-header {
  /* Width mode: --lesson-header-align 1 = cap at --lesson-max-w (aligned with
     the body text, default), 0 = 100% (full column). Same calc flag as topbar. */
  max-width: calc(var(--lesson-max-w, 1600px) * var(--lesson-header-align, 1) + 100% * (1 - var(--lesson-header-align, 1)));
  margin-inline: auto; width: 100%;
  padding: 26px 40px 0;
  background: var(--lesson-header-bg, #f6f7f9);
  border-bottom: var(--lesson-header-border-w, 1px) solid var(--lesson-header-border-color, var(--border));
}
.chapter-layout.has-nav main { max-width: var(--lesson-max-w, 1600px); margin: 0 auto; padding: 6px 40px 40px; }
.chapter-layout.has-nav .pager { max-width: var(--lesson-max-w, 1600px); margin: 0 auto; padding: 0 40px 64px; }
@media (max-width: 767.98px) {
  .chapter-layout.has-nav .lesson-header { padding: 20px 20px 0; }
  .chapter-layout.has-nav main { padding: 6px 20px 32px; }
  .chapter-layout.has-nav .pager { padding: 0 20px 48px; }
}

/* Course-tree sidebar. Never hidden — it must stay a grid item to hold its
   column (see the .chapter-body grid-column pin above); it renders empty until
   _courseNav.js has rows for it. */
.course-nav {
  background: var(--course-nav-bg, #f8fafc);
  position: relative; z-index: 1;
}
@media (min-width: 1024px) {
  .course-nav {
    position: sticky; top: 0;
    max-height: 100vh; overflow-y: auto;
    /* No border-right here: the full-height .col-resizer--nav line is the
       divider, so it reaches the end of the page (this sticky column is only
       as tall as its own content). */
  }
}
.course-nav-inner { padding: 20px 16px 24px; display: flex; flex-direction: column; gap: 6px; }
.course-nav-label {
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 10px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--text3); font-weight: 600; padding: 0 4px 8px;
}

/* Sidebar brand row (AcademeX wordmark) */
.cn-brand {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 6px 16px;
}
.cn-brand-icon {
  width: 30px; height: 30px; flex: none; border-radius: 9px;
  background: var(--accent); /* fallback where color-mix is unsupported */
  background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 62%, white));
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 6px 16px -6px var(--accent);
  font-family: var(--font-mono, 'Consolas', monospace);
  font-weight: 700; color: #fff; font-size: 15px;
}
.cn-brand-name {
  font-family: var(--font-display, inherit); font-weight: 700;
  color: var(--text); font-size: 16px; letter-spacing: -0.01em;
}
.cn-brand-name em { font-style: normal; color: var(--accent); }

/* Course card: identity row + collapsible pace panel */
.cn-course-card {
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--card); margin-bottom: 12px; overflow: hidden;
}
.cn-course-head {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 12px 14px; background: none; border: none; cursor: pointer;
  font-family: inherit; font-size: inherit; text-align: start;
}
.cn-course-head:hover { background: var(--bg2); }
.cn-course-icon {
  width: 34px; height: 34px; flex: none; border-radius: 9px;
  background: rgba(var(--accent-rgb), 0.12);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 12px; font-weight: 700; color: var(--accent);
}
.cn-course-titles { min-width: 0; flex: 1; }
.cn-course-title {
  display: block; /* inline spans never ellipsize/stack */
  font-family: var(--font-display, inherit); font-weight: 700;
  color: var(--text); font-size: 14px; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cn-course-kicker {
  display: block; margin-top: 2px;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 10px; color: var(--text3); letter-spacing: 0.06em;
  text-transform: uppercase;
}
.cn-course-caret { color: var(--text3); font-size: 11px; flex: none; }
.cn-course-card.open .cn-course-title { white-space: normal; }

/* Collapsed summary strip (mini bar + "Wk 2 · 9% · on track") — shown in
   place of the pace panel while the card is closed. */
.cn-pace-mini {
  display: flex; align-items: center; gap: 10px;
  padding: 0 14px 12px;
}
.cn-pace-mini .pace-bar { flex: 1; height: 4px; margin-top: 0; }
.cn-pace-mini-meta {
  flex: none; font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 11px; color: var(--text3); white-space: nowrap;
}
.cn-pace-mini-meta b { font-weight: 600; }
.cn-pace-mini-meta b.ok { color: var(--ok); }
.cn-pace-mini-meta b.behind { color: var(--warn); }

/* Pace panel (inside the course card, below a hairline). Rendered only
   when the course carries termStartDate/termWeeks. */
.cn-pace { border-top: 1px solid var(--border); padding: 12px 14px 14px; }
.cn-pace[hidden] { display: none; }
.pace-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  font-size: 13.5px; color: var(--text2);
}
.pace-row b { color: var(--text); font-weight: 700; }
.pace-bar {
  position: relative; height: 6px; border-radius: 99px;
  background: var(--bg3); margin-top: 10px;
}
.pace-bar-fill {
  position: absolute; inset-inline-start: 0; top: 0; height: 100%;
  border-radius: 99px; background: var(--accent);
}
.pace-bar-tick {
  position: absolute; top: -3px; width: 2px; height: 12px;
  border-radius: 2px; background: var(--text4);
}
.pace-status {
  display: flex; align-items: center; gap: 7px; margin-top: 12px;
  font-size: 13px; color: var(--text); font-weight: 500;
}
.pace-status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); flex: none; }
.pace-status.behind .pace-status-dot { background: var(--warn); }
.pace-legend {
  display: flex; align-items: center; gap: 14px;
  border-top: 1px solid var(--border); margin-top: 12px; padding-top: 10px;
  font-size: 11px; color: var(--text3);
}
.pace-legend span { display: inline-flex; align-items: center; gap: 6px; }
.pace-legend .lg-you { width: 10px; height: 5px; border-radius: 2px; background: var(--accent); flex: none; }
.pace-legend .lg-expected { width: 10px; height: 2px; background: var(--text4); flex: none; }

/* Locked chapter rows (unlockWeek in the future — student view) */
.course-nav-chapter.locked {
  border-style: dashed; background: var(--bg2); opacity: 0.75;
}
.course-nav-chapter.locked .cn-head { cursor: default; }
.course-nav-chapter.locked .cn-head:hover { background: none; }
.course-nav-chapter.locked .cn-title { color: var(--text3); font-weight: 600; }
.cn-lock-icon { color: var(--text3); display: inline-flex; }
.cn-unlock-label {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 9px; letter-spacing: 0.05em; color: var(--text4);
  white-space: nowrap; flex-shrink: 0; text-transform: uppercase;
}
.course-nav-chapter {
  border: var(--cnchap-bd-w, 1px) solid var(--cnchap-bd-c, var(--border));
  border-radius: var(--cnchap-radius, 12px);
  background: var(--cnchap-bg, var(--card));
  box-shadow: var(--cnchap-shadow, none);
  margin-bottom: var(--cnchap-mb, 0px);
  overflow: hidden;
}
.course-nav-chapter.current {
  border-color: rgba(var(--accent-rgb), 0.35);
  box-shadow: 0 10px 24px -16px rgba(var(--accent-rgb), 0.45);
}
.cn-head {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: var(--cnchap-pad-y, 10px) var(--cnchap-pad-x, 12px);
  background: none; border: none; cursor: pointer;
  font-family: inherit; font-size: inherit; text-align: start;
}
.cn-head:hover { background: var(--bg2); }
.course-nav-chapter.current .cn-head {
  background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.07), transparent);
}
/* Chapter number in a completion ring: the conic gradient is the ring
   track/fill (fill % arrives as the --cn-p inline var from _courseNav.js
   — dynamic data, like block widthPct); .cn-num-inner is the disc that
   holds the number. Completed chapters collapse to a solid --ok disc. */
.cn-num {
  width: 26px; height: 26px; flex-shrink: 0; border-radius: 50%;
  background: conic-gradient(var(--accent) var(--cn-p, 0%), var(--bg3) 0);
  padding: 2px;
  display: flex; align-items: center; justify-content: center;
}
.cn-num-inner {
  width: 100%; height: 100%; border-radius: 50%;
  background: var(--card);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 11px; font-weight: 700;
  color: var(--text3);
}
.course-nav-chapter.current .cn-num-inner { color: var(--accent); }
.course-nav-chapter.done .cn-num { background: var(--ok); padding: 0; }
.course-nav-chapter.done .cn-num-inner { background: transparent; color: #fff; }
.cn-title {
  flex: 1; min-width: 0;
  font-family: var(--cnchap-font, var(--font-display, inherit));
  font-weight: var(--cnchap-fw, 700); font-size: var(--cnchap-fs, 14px);
  color: var(--cnchap-fg, var(--text)); line-height: 1.35;
}
.cn-draft {
  display: inline-block; margin-inline-start: 6px; padding: 1px 6px; border-radius: 4px;
  background: rgba(var(--warn-rgb), 0.15); color: var(--warn);
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 9px; letter-spacing: 1px; font-weight: 700;
  vertical-align: middle;
}
.cn-progress { font-family: var(--font-mono, 'Consolas', monospace); font-size: 10.5px; color: var(--text3); flex-shrink: 0; }
.cn-caret { color: var(--text3); font-size: 10px; flex-shrink: 0; }
.course-nav-lessons { padding: 2px 8px 8px; display: flex; flex-direction: column; gap: 2px; }
.course-nav-lessons[hidden] { display: none; }
.course-nav-lesson {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 9px; border-radius: 9px; border: none; background: none;
  cursor: pointer; font-family: inherit; font-size: var(--cnlesson-name-fs, 14px);
  color: var(--cnlesson-name-fg, var(--text2)); text-align: start; text-decoration: none; line-height: 1.4;
}
/* Numbered lesson state circle (todo n / done ✓ / active halo). */
.cn-lesson-num {
  width: 20px; height: 20px; flex-shrink: 0; border-radius: 50%;
  border: 1px solid var(--border); background: var(--bg2);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono, 'Consolas', monospace); font-size: var(--cnlesson-num-fs, 10px); font-weight: 700;
  color: var(--cnlesson-num-fg, var(--text3));
}
.course-nav-lesson:hover { background: var(--bg2); color: var(--text); }
.course-nav-lesson.done .cn-lesson-num {
  background: rgba(var(--ok-rgb), 0.12); border-color: transparent; color: var(--ok);
}
.course-nav-lesson.active { background: rgba(var(--accent-rgb), 0.08); color: var(--accent); font-weight: 600; }
.course-nav-lesson.active .cn-lesson-num {
  background: var(--accent); border-color: var(--accent); color: #fff;
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.25);
}
.cn-lesson-label { flex: 1; min-width: 0; }
.cn-lesson-meta { font-family: var(--font-mono, 'Consolas', monospace); font-size: var(--cnlesson-meta-fs, 10px); color: var(--cnlesson-meta-fg, var(--text3)); flex-shrink: 0; }

/* Resume chip — sidebar footer + hub banner (`--inline` modifier) */
.resume-chip {
  display: flex; align-items: center; gap: 10px; margin-top: 10px;
  padding: 10px 12px; border-radius: 12px;
  border: 1px solid rgba(var(--accent-rgb), 0.3);
  background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.10), rgba(var(--accent-rgb), 0.05));
  text-decoration: none; color: var(--text); transition: background 0.2s, box-shadow 0.2s;
}
.resume-chip:hover { background: rgba(var(--accent-rgb), 0.14); box-shadow: 0 8px 18px -10px rgba(var(--accent-rgb), 0.6); }
.resume-chip--inline { display: inline-flex; margin: 0 0 20px; }
.resume-chip-icon {
  width: 30px; height: 30px; flex-shrink: 0; border-radius: 9px;
  background: var(--accent); color: #fff;
  display: flex; align-items: center; justify-content: center; font-size: 11px;
  box-shadow: 0 6px 16px -6px rgba(var(--accent-rgb), 0.8);
}
.resume-chip-kicker {
  display: block; font-family: var(--font-mono, 'Consolas', monospace); font-size: 9.5px;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); font-weight: 700;
}
.resume-chip-title {
  display: block; font-family: var(--font-display, inherit);
  font-weight: 700; font-size: 0.85rem; color: var(--text); line-height: 1.3;
}

/* Sticky lesson top bar (breadcrumb chips / lesson search / avatar) —
   rendered by _courseNav.js renderTopbar() when the chrome boots. */
.lesson-topbar[hidden] { display: none !important; }
.lesson-topbar {
  position: sticky; top: 0; z-index: 5;
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
  padding: 13px 40px;
  background: var(--topbar-bg, #f6f7f9);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: var(--topbar-border-w, 1px) solid var(--topbar-border-color, var(--border));
  /* Width mode: --topbar-align 1 = cap at --lesson-max-w (aligned with the body
     text), 0 = 100% (full column). A calc flag keeps it theme-safe (no class). */
  max-width: calc(var(--lesson-max-w, 1600px) * var(--topbar-align, 0) + 100% * (1 - var(--topbar-align, 0)));
  margin-inline: auto;
}
.topbar-crumbs {
  display: flex; gap: 7px; align-items: center; min-width: 0;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 12px; color: var(--text3);
}
.crumb-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--card); cursor: pointer; color: var(--text3);
  font-family: inherit; font-size: inherit; text-decoration: none;
  white-space: nowrap; transition: border-color 0.15s;
}
.crumb-chip:hover { border-color: rgba(var(--accent-rgb), 0.35); color: var(--text2); }
.crumb-chip .chip-caret { font-size: 9px; color: var(--text4); }
.topbar-crumbs .crumb-sep { color: var(--text4); }
.topbar-crumbs .crumb-here {
  color: var(--text2); font-weight: 700; padding: 5px 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Chapter dropdown under the breadcrumb chip */
.crumb-menu[hidden] { display: none !important; }
.crumb-menu {
  position: absolute; top: calc(100% - 4px); inset-inline-start: 40px; z-index: 50;
  min-width: 260px; max-width: 340px; max-height: 60vh; overflow-y: auto;
  background: var(--card); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 18px 50px -18px rgba(15, 20, 32, 0.35);
  padding: 6px; display: flex; flex-direction: column; gap: 1px;
}
.crumb-menu-item {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 10px; border-radius: 8px;
  font-family: var(--font-body, inherit); font-size: 13px;
  color: var(--text2); text-decoration: none;
}
.crumb-menu-item:hover { background: var(--bg3); color: var(--text); }
.crumb-menu-item.current { background: rgba(var(--accent-rgb), 0.08); color: var(--accent); font-weight: 600; }
.crumb-menu-item.locked { color: var(--text3); cursor: default; justify-content: flex-start; }
.crumb-menu-item.locked:hover { background: none; color: var(--text3); }
.crumb-menu-item.locked .cn-unlock-label { margin-inline-start: auto; }
.crumb-menu-num {
  width: 20px; height: 20px; flex: none; border-radius: 50%;
  background: var(--bg3); display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 10px; font-weight: 700;
  color: var(--text3);
}
@media (max-width: 767.98px) {
  .crumb-menu { inset-inline: 12px; max-width: none; }
}

.topbar-search {
  margin-inline-start: auto;
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--card); cursor: pointer;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 12px; color: var(--text3);
  flex: 0 1 180px; min-width: 0;
  transition: border-color 0.15s;
}
.topbar-search:hover { border-color: rgba(var(--accent-rgb), 0.35); }
.topbar-search kbd {
  margin-inline-start: auto;
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 10px;
  background: var(--bg3); border: 1px solid var(--border); border-radius: 5px;
  padding: 1px 6px; color: var(--text3);
}
.topbar-avatar {
  width: 32px; height: 32px; flex: none; border-radius: 50%;
  background: var(--accent); /* fallback where color-mix is unsupported */
  background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 62%, white));
  color: #fff; display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display, inherit); font-weight: 700; font-size: 13px;
}
.lesson-topbar .nav-drawer-toggle { margin: 0; }
@media (max-width: 767.98px) {
  .lesson-topbar { padding: 10px 20px; }
  .topbar-crumbs .crumb-chip { max-width: 34vw; overflow: hidden; }
}

/* The in-header `.crumbs` breadcrumb was retired 2026-08-05 together with the
   classic chapter layout — `.topbar-crumbs` above is the one breadcrumb
   component (see docs/design-system.md). `.crumb-sep` / `.crumb-chip` /
   `.crumb-menu*` all live with it. */

/* Prev–next pager (after the lesson body; injected by _courseNav.js) */
.pager {
  display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
  max-width: 1200px; margin: -40px auto 0; padding: 0 60px 80px;
  position: relative; z-index: 1;
}
.pager-card {
  display: block; border: 1px solid var(--border); border-radius: 10px;
  padding: 16px 18px; background: var(--card);
  text-decoration: none; color: inherit;
  transition: border-color 0.18s, transform 0.18s, box-shadow 0.18s;
  box-shadow: 0 1px 2px rgba(15,20,32,.03);
}
.pager-card:hover {
  transform: translateY(-2px); border-color: rgba(var(--accent-rgb), 0.3);
  box-shadow: 0 10px 24px -16px rgba(var(--accent-rgb), 0.45);
}
.pager-card.next {
  text-align: end;
  border-color: rgba(var(--accent-rgb), 0.3);
  background: linear-gradient(180deg, var(--card), rgba(var(--accent-rgb), 0.04));
}
.pager-card.empty { border: none; background: none; box-shadow: none; pointer-events: none; }
.pager-label {
  display: flex; align-items: center; gap: 7px;
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 10px;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--text3);
  font-weight: 600; margin-bottom: 6px;
}
.pager-card.next .pager-label { justify-content: flex-end; }
/* ←/→ keycaps — arrow keys really navigate (see _courseNav.js bindKeys). */
.pager-label kbd {
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 10px;
  background: var(--bg3); border: 1px solid var(--border); border-bottom-width: 2px;
  border-radius: 5px; padding: 1px 6px; color: var(--text3); letter-spacing: 0;
}
.pager-title {
  display: block; font-family: var(--font-display, inherit);
  font-weight: 700; font-size: 0.98rem; color: var(--text);
}
.pager-card.next .pager-title { color: var(--accent); }
.pager-chapter { display: block; font-family: var(--font-mono, 'Consolas', monospace); font-size: 10.5px; color: var(--text3); font-weight: 400; margin-bottom: 2px; }
@media (max-width: 768px) {
  .pager { padding: 0 20px 60px; }
}
@media (max-width: 640px) {
  .pager { grid-template-columns: 1fr; }
  .pager-card.empty { display: none; }
}

/* Mobile drawer (<1024px): the sidebar slides in from the left, opened
   by the ☰ toggle _courseNav.js injects into the sticky pill bar. */
.nav-drawer-toggle { display: none; }
.course-nav-overlay[hidden] { display: none !important; }
.course-nav-overlay { display: none; }
@media (max-width: 1023.98px) {
  .nav-drawer-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0; font-size: 16px; line-height: 1; padding: 8px 14px;
    border: 1px solid var(--border); border-radius: 10px;
    background: var(--bg2); color: var(--text2); cursor: pointer;
    box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  }
  .nav-drawer-toggle:hover { border-color: var(--accent); color: var(--accent); }
  .course-nav {
    position: fixed; top: 0; inset-inline-start: 0; bottom: 0;
    width: min(320px, 85vw); z-index: 300;
    /* RTL: this slide-in transform is physical (no logical-transform exists);
       add a [dir=rtl] override → translateX(105%) when RTL is enabled. */
    transform: translateX(-105%); transition: transform 0.25s ease;
    overflow-y: auto; border-inline-end: 1px solid var(--border);
    box-shadow: 0 24px 60px -12px rgba(15, 23, 42, 0.25);
  }
  body.course-nav-open { overflow: hidden; }
  body.course-nav-open .course-nav { transform: translateX(0); }
  body.course-nav-open .course-nav-overlay {
    display: block; position: fixed; inset: 0;
    background: rgba(30,41,59,0.4); z-index: 290;
  }
}

/* ── Right rail: "On this page" TOC + lesson read % + Mark complete ──
   Rendered by courses/_shared/_lessonRail.js; ≥1280px only (the grid's
   third column). Named standard components — tokens only. */
.lesson-rail[hidden] { display: none !important; }
.lesson-rail { display: none; }
@media (min-width: 1280px) {
  .lesson-rail {
    display: flex; flex-direction: column;
    position: sticky; top: 0; max-height: 100vh; overflow-y: auto;
    background: var(--lesson-rail-bg, #f6f7f9);
    /* No border-left here: the full-height .col-resizer--rail line is the
       divider (see .course-nav above). */
    padding: 26px 20px;
  }
}
.rail-label {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text3); margin-bottom: 14px;
}
.rail-toc { display: flex; flex-direction: column; }
.rail-toc a {
  font-size: 13px; color: var(--text2); text-decoration: none;
  padding: 6px 0; padding-inline-start: 14px; border-inline-start: 2px solid var(--border);
  line-height: 1.45; transition: color 0.15s, border-color 0.15s;
}
.rail-toc a:hover { color: var(--text); }
.rail-toc a.active {
  color: var(--accent); font-weight: 600;
  border-inline-start-color: var(--accent);
}
.rail-toc .rail-toc-empty { font-size: 12.5px; color: var(--text4); padding-inline-start: 14px; }
.rail-divider { border-top: 1px solid var(--border); margin: 22px 0 18px; }
.rail-stat {
  display: flex; align-items: baseline; gap: 7px; margin-top: 8px;
}
.rail-stat-value {
  font-family: var(--font-display, inherit); font-weight: 600;
  font-size: 20px; color: var(--text); letter-spacing: -0.02em;
}
.rail-stat-caption { font-size: 13px; color: var(--text2); }
.rail-read-bar {
  height: 5px; border-radius: 99px; background: var(--bg3);
  overflow: hidden; margin-top: 10px;
}
.rail-read-bar-fill {
  display: block; height: 100%; border-radius: 99px; width: 0%;
  background: var(--accent); /* fallback where color-mix is unsupported */
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 62%, white));
  transition: width 0.2s;
}
.rail-complete-btn {
  margin-top: 24px; width: 100%;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-body, inherit); font-weight: 600; font-size: 13.5px;
  border-radius: 10px; padding: 11px; cursor: pointer;
  background: rgba(var(--accent-rgb), 0.10); color: var(--accent);
  border: 1px solid rgba(var(--accent-rgb), 0.30);
  transition: transform 0.15s, filter 0.15s;
}
.rail-complete-btn:hover { filter: brightness(1.04); }
.rail-complete-btn:active { transform: scale(0.98); }
.rail-complete-btn.done {
  background: rgba(var(--ok-rgb), 0.12);
  color: var(--ok); /* fallback where color-mix is unsupported */
  color: color-mix(in srgb, var(--ok) 72%, black);
  border-color: rgba(var(--ok-rgb), 0.35);
}
.rail-complete-btn[hidden] { display: none; }

/* ── Course Tutor (courses/_shared/_aiTutor.js) ─────────────────────
   Student AI chat sharing column 3 with the rail. Structure:
     .rail-col > .lesson-rail + .ai-tutor        (#aiTutorOverlay is a
   sibling of .rail-col for the <1280px drawer). The wrapper is
   display:contents by default so the 1024-1279px two-column grid never
   gains a phantom third item; ≥1280px it BECOMES the grid's third column
   (taking over the rail's sticky role) and stacks rail + tutor. Named
   standard components — tokens only. */
.rail-col { display: contents; }
.ai-tutor[hidden] { display: none !important; }
.ai-tutor { display: none; flex-direction: column; }
.ai-tutor-overlay[hidden] { display: none !important; }
.ai-tutor-overlay { display: none; }
@media (min-width: 1280px) {
  .chapter-layout.has-nav .rail-col {
    display: flex; flex-direction: column; min-width: 0;
    position: sticky; top: 0; max-height: 100vh; overflow: hidden;
    background: var(--lesson-rail-bg, #f6f7f9);
  }
  /* Inside the wrapper the rail stands down from sticky duty and shares
     the viewport: it keeps its natural height (scrolling internally when
     squeezed) and the tutor flexes into the rest. */
  .chapter-layout.has-nav .rail-col .lesson-rail {
    position: static; max-height: none; overflow-y: auto;
    flex: 0 1 auto; min-height: 0;
  }
  .chapter-layout.has-nav .rail-col .ai-tutor {
    display: flex; flex: 1 1 55%; min-height: 220px;
    border-top: 1px solid var(--border);
  }
  .chapter-layout.has-nav .rail-col .ai-tutor.ait-collapsed {
    flex: 0 0 auto; min-height: 0;
  }
}
/* <1280px: slide-over drawer from the end edge, opened by the topbar
   "✦ Tutor" button (mirror of the course-nav drawer above). */
@media (max-width: 1279.98px) {
  .ai-tutor {
    display: flex; position: fixed; top: 0; inset-inline-end: 0; bottom: 0;
    width: min(400px, 92vw); z-index: 320;
    background: var(--lesson-rail-bg, #f6f7f9);
    border-inline-start: 1px solid var(--border);
    box-shadow: 0 24px 60px -12px rgba(15, 23, 42, 0.25);
    /* RTL: physical transform (no logical-transform exists); add a
       [dir=rtl] override → translateX(-105%) when RTL is enabled. */
    transform: translateX(105%); transition: transform 0.25s ease;
  }
  body.ai-tutor-open { overflow: hidden; }
  body.ai-tutor-open .ai-tutor { transform: translateX(0); }
  body.ai-tutor-open .ai-tutor-overlay {
    display: block; position: fixed; inset: 0;
    background: rgba(30,41,59,0.4); z-index: 310;
  }
}
/* Topbar trigger — visible only while the panel lives in the drawer. */
.ait-topbar-btn { display: none; }
@media (max-width: 1279.98px) {
  .ait-topbar-btn {
    display: inline-flex; align-items: center; justify-content: center;
    gap: 6px; flex-shrink: 0; font-size: 13px; font-weight: 600;
    line-height: 1; padding: 8px 12px;
    border: 1px solid rgba(var(--accent-rgb), 0.30); border-radius: 10px;
    background: rgba(var(--accent-rgb), 0.08); color: var(--accent);
    cursor: pointer; font-family: var(--font-body, inherit);
  }
  .ait-topbar-btn:hover { filter: brightness(1.05); }
}
/* Header strip */
.ait-header {
  flex: none; display: flex; align-items: center; gap: 6px;
  padding: 12px 14px; border-bottom: 1px solid var(--border);
  position: relative;
}
.ait-title {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text3); white-space: nowrap; overflow: hidden;
}
.ait-title .ait-spark { color: var(--accent); font-size: 13px; flex: none; }
.ait-mode {
  flex: none; display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 600; font-family: var(--font-body, inherit);
  padding: 4px 8px; border-radius: 999px; cursor: pointer;
  background: rgba(var(--accent-rgb), 0.08); color: var(--accent);
  border: 1px solid rgba(var(--accent-rgb), 0.25);
}
.ait-mode:hover { filter: brightness(1.05); }
.ait-icon-btn {
  flex: none; background: none; border: none; cursor: pointer;
  color: var(--text3); padding: 4px 6px; border-radius: 6px;
  font-size: 13px; line-height: 1;
}
.ait-icon-btn:hover { background: var(--bg3); color: var(--text); }
.ait-menu {
  position: absolute; top: calc(100% - 4px); inset-inline-end: 10px;
  z-index: 30; min-width: 220px;
  background: var(--card); border: 1px solid var(--border);
  border-radius: 10px; box-shadow: 0 12px 32px -8px rgba(15, 20, 32, 0.25);
  padding: 4px; display: flex; flex-direction: column;
}
.ait-menu[hidden] { display: none !important; }
.ait-menu-item {
  display: block; width: 100%; text-align: start; border: none;
  background: none; cursor: pointer; font-family: inherit;
  font-size: 12.5px; color: var(--text2); padding: 7px 9px; border-radius: 7px;
}
.ait-menu-item:hover { background: rgba(var(--accent-rgb), 0.08); color: var(--text); }
.ait-menu-item.selected { color: var(--accent); font-weight: 600; }
.ait-menu-item .ait-menu-sub { display: block; font-size: 11px; color: var(--text4); font-weight: 400; }
/* Transcript */
.ait-transcript {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: 14px; display: flex; flex-direction: column; gap: 10px;
}
.ait-msg {
  max-width: 94%; padding: 9px 12px; border-radius: 12px;
  font-size: 13.5px; line-height: 1.55; color: var(--text);
  overflow-wrap: break-word; min-width: 0;
}
.ait-msg.user {
  align-self: flex-end;
  background: rgba(var(--accent-rgb), 0.10);
  border: 1px solid rgba(var(--accent-rgb), 0.18);
  border-end-end-radius: 4px;
}
.ait-msg.assistant {
  align-self: flex-start; background: var(--card);
  border: 1px solid var(--border); border-end-start-radius: 4px;
}
.ait-msg.system {
  align-self: center; background: none; border: none;
  color: var(--text3); font-size: 12.5px; text-align: center; padding: 2px 6px;
}
.ait-msg p { margin: 0 0 6px; }
.ait-msg p:last-child { margin-bottom: 0; }
.ait-msg ul, .ait-msg ol { margin: 6px 0; padding-inline-start: 18px; }
.ait-msg li { margin: 2px 0; }
/* Replies come from rich-text.js with newlines intact — honor them. */
.ait-msg .ait-prose { white-space: pre-wrap; }
.ait-msg .ait-prose .q-code { white-space: pre-wrap; }
/* Collapse toggles the desktop column panel; ✕ closes the mobile drawer. */
.ait-close { display: none; }
@media (max-width: 1279.98px) {
  .ait-collapse { display: none; }
  .ait-close { display: inline-flex; }
}
/* rich-text.js injects .q-code / .q-code-inline styling; just keep code
   from blowing the bubble width. */
.ait-msg .q-code { max-width: 100%; overflow-x: auto; }
.ait-quote {
  font-size: 12px; color: var(--text3);
  border-inline-start: 2px solid rgba(var(--accent-rgb), 0.4);
  padding-inline-start: 8px; margin-bottom: 6px;
  white-space: pre-wrap; max-height: 88px; overflow: hidden;
}
.ait-retry {
  margin-top: 6px; font-size: 12px; font-weight: 600; cursor: pointer;
  background: none; border: 1px solid var(--border); border-radius: 7px;
  color: var(--text2); padding: 4px 10px; font-family: inherit;
}
.ait-retry:hover { border-color: var(--accent); color: var(--accent); }
/* Suggestion chips (empty state + follow-ups) */
.ait-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.ait-followups { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; align-self: flex-start; max-width: 94%; }
.ait-chip {
  font-size: 12px; font-weight: 500; font-family: var(--font-body, inherit);
  padding: 5px 10px; border-radius: 999px; cursor: pointer; text-align: start;
  background: rgba(var(--accent-rgb), 0.07); color: var(--accent);
  border: 1px solid rgba(var(--accent-rgb), 0.25);
  transition: filter 0.15s;
}
.ait-chip:hover { filter: brightness(1.06); background: rgba(var(--accent-rgb), 0.12); }
.ait-empty { color: var(--text3); font-size: 13px; line-height: 1.6; }
.ait-empty .ait-empty-hi { color: var(--text); font-weight: 600; display: block; margin-bottom: 4px; }
/* Typing indicator */
.ait-typing { align-self: flex-start; display: inline-flex; gap: 4px; padding: 12px 12px; }
.ait-typing i {
  width: 6px; height: 6px; border-radius: 50%; background: var(--text3);
  animation: ait-blink 1.2s infinite;
}
.ait-typing i:nth-child(2) { animation-delay: 0.2s; }
.ait-typing i:nth-child(3) { animation-delay: 0.4s; }
@keyframes ait-blink { 0%, 80%, 100% { opacity: 0.25; } 40% { opacity: 1; } }
/* "My own AI" prompt card (mode:'prompt' result) */
.ait-prompt-card {
  align-self: stretch; padding: 10px 12px; border-radius: 12px;
  border: 1px dashed rgba(var(--accent-rgb), 0.4);
  background: rgba(var(--accent-rgb), 0.05);
  font-size: 12.5px; color: var(--text2);
}
.ait-prompt-card pre {
  margin: 8px 0; padding: 8px; max-height: 120px; overflow: auto;
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 11px;
  background: var(--bg3); border-radius: 8px; white-space: pre-wrap;
  color: var(--text2);
}
.ait-copy-btn {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-body, inherit); font-weight: 600; font-size: 12.5px;
  border-radius: 8px; padding: 7px 12px; cursor: pointer;
  background: rgba(var(--accent-rgb), 0.10); color: var(--accent);
  border: 1px solid rgba(var(--accent-rgb), 0.30);
}
.ait-copy-btn:hover { filter: brightness(1.04); }
/* Composer */
.ait-composer {
  flex: none; display: flex; gap: 8px; align-items: flex-end;
  padding: 10px 12px; border-top: 1px solid var(--border);
  background: var(--card);
}
.ait-input {
  flex: 1; min-width: 0; resize: none; outline: none;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg2); padding: 8px 10px; max-height: 110px;
  font-family: var(--font-body, inherit); font-size: 13.5px;
  line-height: 1.45; color: var(--text);
}
.ait-input:focus { border-color: rgba(var(--accent-rgb), 0.5); }
.ait-send {
  flex: none; width: 34px; height: 34px; border-radius: 10px; border: none;
  background: var(--accent); color: #fff; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 14px; line-height: 1;
}
.ait-send:disabled { opacity: 0.5; cursor: default; }
.ait-collapsed .ait-transcript,
.ait-collapsed .ait-followups,
.ait-collapsed .ait-composer { display: none; }
/* Selection popover ("Ask AI" quick actions) */
.ait-popover[hidden] { display: none !important; }
.ait-popover {
  position: fixed; z-index: 380; max-width: 340px;
  display: flex; flex-wrap: wrap; gap: 2px; padding: 5px;
  background: var(--card); border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 12px 40px -8px rgba(15, 20, 32, 0.3);
}
.ait-pop-btn {
  font-family: inherit; font-size: 12px; border: none; background: none;
  color: var(--text2); padding: 6px 9px; border-radius: 8px; cursor: pointer;
}
.ait-pop-btn:hover { background: rgba(var(--accent-rgb), 0.08); color: var(--accent); }
.ait-pop-btn.primary { color: var(--accent); font-weight: 600; }
/* Per-question quiz help trigger */
.ait-quiz-help-btn {
  margin-top: 12px; font-family: inherit; font-size: 12.5px;
  background: none; border: 1px dashed rgba(var(--accent-rgb), 0.35);
  color: var(--accent); border-radius: 8px; padding: 6px 10px; cursor: pointer;
}
.ait-quiz-help-btn:hover { background: rgba(var(--accent-rgb), 0.07); }

/* ── ⌘K lesson search overlay (courses/_shared/_courseSearch.js) ── */
.cmdk-overlay[hidden] { display: none !important; }
.cmdk-overlay {
  position: fixed; inset: 0; z-index: 400;
  background: rgba(15, 20, 32, 0.4);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 12vh 16px 16px;
}
.cmdk-panel {
  width: 100%; max-width: 560px;
  background: var(--card); border: 1px solid var(--border);
  border-radius: 14px; overflow: hidden;
  box-shadow: 0 24px 70px -18px rgba(15, 20, 32, 0.45);
}
.cmdk-input-row {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 16px; border-bottom: 1px solid var(--border);
}
.cmdk-input-row svg { flex: none; stroke: var(--text3); }
.cmdk-input {
  flex: 1; border: none; outline: none; background: none;
  font-family: var(--font-body, inherit); font-size: 15px; color: var(--text);
}
.cmdk-input::placeholder { color: var(--text4); }
.cmdk-esc {
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 10px;
  background: var(--bg3); border: 1px solid var(--border); border-radius: 5px;
  padding: 1px 6px; color: var(--text3);
}
.cmdk-list { max-height: 46vh; overflow-y: auto; padding: 6px; }
.cmdk-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 10px; border-radius: 9px; border: none; background: none;
  cursor: pointer; font-family: inherit; text-align: start;
  color: var(--text2); text-decoration: none;
}
.cmdk-item.selected, .cmdk-item:hover { background: rgba(var(--accent-rgb), 0.08); }
.cmdk-item-label { flex: 1; min-width: 0; font-size: 14px; color: var(--text); }
.cmdk-item-kicker {
  display: block;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text3);
}
.cmdk-item-meta {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 10px; color: var(--text4); flex: none;
}
.cmdk-item.locked { opacity: 0.55; cursor: default; }
.cmdk-empty { padding: 18px 14px; font-size: 13.5px; color: var(--text3); text-align: center; }

/* Hub card progress pill (inside .card-stats) */
.hub-progress {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 8px; border-radius: 20px;
  font-family: var(--font-mono, 'Consolas', monospace);
  border: 1px solid rgba(var(--accent-rgb), 0.25);
  background: rgba(var(--accent-rgb), 0.08); color: var(--accent); font-weight: 700;
}
.hub-progress.done {
  border-color: rgba(var(--ok-rgb), 0.3);
  background: rgba(var(--ok-rgb), 0.1); color: var(--ok);
}

/* ════════════════════════════════════════════════════════════
   Present mode (instructor projector view — courses/_shared/_presentMode.js)
   + the slideBreak block marker. Named standard components; see
   docs/design-system.md and docs/pages/courses/_shared-chapter.md.

   z-index ladder (page-level; the native top layer — the try-it
   <dialog> — always stacks above all of these):
     content < .present-frame (8400) < .present-ink (8500)
     < .present-notes (8600) < .present-laser (8700)
     < .present-nav/.present-counter (8800)
     < .present-outline (8950) < .present-toolbar (9000)
     < .present-blackout (9500)
   (The v3 slide frame — .present-pageprog/-band/-foot — rides its own
   .present-frame layer. It sits above slide content because it is page
   furniture the content scrolls UNDER, and directly below the ink so
   the presenter can still annotate over the foot and `B` still blacks
   it out.)
   The laser sits UNDER the chrome on purpose: reaching for a toolbar
   button should not drag a glowing dot across it.
   ════════════════════════════════════════════════════════════ */

/* slideBreak block: invisible zero-height marker in read mode… */
.slide-break { height: 0; overflow: hidden; }
/* …but a visible, selectable divider in the visual editor. */
.slide-break--editing {
  height: auto; overflow: visible;
  display: flex; align-items: center; gap: 10px;
  margin: 10px 0; color: var(--text3);
}
.slide-break--editing::before,
.slide-break--editing::after {
  content: ''; flex: 1; border-top: 2px dashed var(--border);
}
.slide-break--editing span {
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 11px; font-weight: 600;
  letter-spacing: 1.5px; text-transform: uppercase;
  padding: 2px 10px; border: 1px dashed var(--border); border-radius: 20px;
  background: var(--bg2); white-space: nowrap;
}

/* Staff-only launch button, injected into the chapter header (which sits
   inside .chapter-body since the 2026-07-18 redesign — hence no `>`). */
#chapter-content header { position: relative; }
.present-launch {
  position: absolute; top: 18px; inset-inline-end: 22px;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 16px; border: 2px solid var(--border); border-radius: 8px;
  background: var(--card); color: var(--text2);
  font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
  transition: all 0.2s;
}
.present-launch:hover {
  border-color: var(--accent); color: var(--accent);
  box-shadow: 0 4px 12px rgba(var(--accent-rgb), 0.15);
}

/* ── Mode root: body.presenting ─────────────────────────────── */
body.presenting {
  /* FIXED SLIDE GEOMETRY (2026-07-23). Present mode derives NOTHING from
     the live viewport any more. `geometry()` in _presentMode.js resolves
     the whole slide ONCE against a 1920×1080 reference projector and
     publishes the result — in design px — as the custom properties below
     (written on document.body). The stage is then fitted to the real
     screen with a single `transform: scale(--present-fit)`, and a
     transform scales pixels AFTER layout: no screen size, fullscreen
     state or browser zoom can re-wrap text or move a slide break.
     The values here are the unthemed 1080p fallbacks (used only in the
     moment before JS writes the real ones).

     Admin-themable knobs (Admin › Appearance › Presentation, via
     settings/theme → theme-loader at :root): --present-margin-x/-y/-b
     (side / top / bottom; -b unset === mirror -y), the panel padding trio
     --present-panel-pad (top) / -bottom (unset === mirror top) / -side,
     and --present-zoom-scale. Those INPUT names are deliberately distinct
     from the OUTPUT vars published below — an input called
     --present-panel-pad-x would shadow the body var of the same name.
     geometry() resolves each
     against the REFERENCE projector, so an admin change is global and
     deterministic — never per-screen. KEEP IN SYNC: the clamp defaults
     now live in geometry() (this file no longer restates them), and
     --present-zoom-scale is the only magnification knob — the old
     viewport-stepped CSS `zoom` on the content column is gone, the fit
     transform does that job. */
  --present-canvas-w: 1422px;      /* 1920 / 1.35 — the design canvas */
  --present-canvas-h: 800px;       /* 1080 / 1.35 */
  --present-col-w: 1308px;         /* content column inside the canvas */
  --present-pad-y: 32px;           /* canvas top margin */
  --present-reserve: 84px;         /* toolbar zone + bottom margin */
  --present-panel-pad-x: 50px;     /* slide panel inner padding (sides) */
  --present-panel-pad-y: 24px;     /* …top */
  --present-panel-pad-b: 24px;     /* …bottom (own admin knob since 2026-07-23) */
  --present-min-h: 684px;          /* slide panel min-height */
  --present-avail-h: 634px;        /* per-slide content budget */
  --present-fit: 1;                /* design px → screen px */
  /* v3 slide-frame heights (2026-07-30) — KEEP IN SYNC with the
     PROG_H / BAND_H / FOOT_H constants in _presentMode.js, which net
     them out of the per-slide budget. */
  --present-prog-h: 4px;           /* on-page deck progress bar */
  --present-band-h: 56px;          /* top band: trail · chip · beats */
  --present-foot-h: 50px;          /* foot: sig · slug · count */
  overflow: hidden;
  background: var(--bg);
}
/* Windowed (non-fullscreen) presenting: a stray viewport scrollbar would
   shrink the client area the stage is fitted to — pin the root too. */
html:has(body.presenting) { overflow: hidden; }
/* Chrome kill: reading-mode furniture disappears while presenting. */
body.presenting #progressBar,
body.presenting #cos102-auth-bar,
body.presenting #chapter-content header,
body.presenting #draft-banner,
body.presenting .course-nav,
body.presenting .course-nav-overlay,
body.presenting .lesson-topbar,
body.presenting .lesson-rail,
body.presenting .rail-col,
body.presenting .ai-tutor,
body.presenting .ai-tutor-overlay,
body.presenting .ait-popover,
body.presenting .cmdk-overlay,
body.presenting .col-resizer,
body.presenting .pager,
body.presenting #fontToolbar,
body.presenting .present-launch { display: none !important; }
/* Replay keeps more reading chrome than Present, but the tutor still
   stands down — its popover/drawer would float over the deck. */
body.replaying .ai-tutor,
body.replaying .ai-tutor-overlay,
body.replaying .ait-popover { display: none !important; }
/* ^ !important: some of these carry runtime INLINE display values
   (student-auth.js sets display:flex on #cos102-auth-bar; the renderer
   toggles #draft-banner inline) — presenting must win over them. */
body.presenting .chapter-layout,
body.presenting .chapter-layout.has-nav { display: block; max-width: none; }
/* In-section headers are fully hidden while presenting (2026-07-30, later
   the same day the subtitle-only demotion landed): every slide is titled by
   the injected leaf-first .present-title block, and each SECTION now opens
   with its own intro slide (.present-divider below) that carries the
   section subtitle at divider scale — a residual subtitle lead on content
   slides would say it twice. paginate() measures the hidden header as 0, so
   every content slide gets a uniform content budget. The editor deck keeps
   real headers (its own CSS). */
body.presenting .section .section-header { display: none; }

/* Leaf-first slide title (2026-07-30; v3 head later the same day),
   injected per section by _presentMode.js while presenting (removed on
   exit; display:none guards a torn state in reading mode). The mockup's
   `.head`: hierarchical ref ("2.3.1", mono) + the slide's own topic on one
   baseline row — parent context lives in the frame band above. One-line
   ellipsis is LOAD-BEARING: constant block height is what lets
   measureAndPaginate() net a single measured value out of every slide's
   budget. */
.present-title { display: none; }
body.presenting .present-title {
  display: flex; align-items: baseline; gap: 14px;
  margin-bottom: 16px;
  white-space: nowrap; overflow: hidden;
}
body.presenting .present-title .ptl-ref {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 15px; color: var(--text3); flex: none;
}
body.presenting .present-title .ptl-title {
  font-family: var(--font-display, inherit);
  font-size: 2.1rem; font-weight: 700; color: var(--text);
  letter-spacing: -0.02em; line-height: 1.15; margin: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── Chapter / section intro slides (2026-07-30; v3 layouts later the
   same day — design/…/present mode/academex-slide-design-v3.html) ──
   One present-owned panel (first child of <main>, injected/removed by
   _presentMode.js dividerEl/removeDivider), rendered per divider record by
   renderDivider(). Chapter COVER: course row · big chapter number + rule +
   title · numbered agenda chips · byline (institution / course). Section
   OPENING: kind chip ("SECTION 2.3") · hero title · lede · numbered
   contents chips. While one shows (body.present-divider-on) the live
   section hides — the divider sits on the same painted page rectangle,
   under the same frame band/foot (its padding clears them). Never
   budget-coupled: titles clamp to 2 lines, the lede to 3, chip rows cap
   their height, so a divider can never overflow and nothing about it is
   measured. display:none at rest also guards a torn state in reading
   mode. Mockup roles ride platform tokens: indigo → --accent,
   display/mono → the --font-* stacks. */
.present-divider { display: none; }
body.presenting.present-divider-on .present-divider {
  display: flex; flex-direction: column; justify-content: center;
  min-height: var(--present-min-h);
  padding: calc(var(--present-panel-pad-y) + var(--present-prog-h, 4px) + var(--present-band-h, 56px))
           var(--present-panel-pad-x)
           calc(var(--present-panel-pad-b) + var(--present-foot-h, 50px));
  animation: fadeSlide 0.4s ease both;
}
/* :not(.present-measuring) is LOAD-BEARING: both rules are !important and
   this one is more specific, so without the guard a repagination firing
   WHILE a divider is up (late font batch, image load) measured the ACTIVE
   section as all-zero rects — its whole content "fit" one slide with
   contentUsed 0 and the live render overflowed the page. */
body.presenting.present-divider-on .section.active:not(.present-measuring) { display: none !important; }
.present-divider .pd-title {
  font-family: var(--font-display, inherit);
  font-weight: 700; color: var(--text); line-height: 1.05;
  letter-spacing: -0.035em; margin: 0;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.present-divider.pd-chapter .pd-title {
  font-size: 4.65rem; max-width: 18ch;   /* mockup 76px × the 1422/1280 canvas ratio */
  letter-spacing: -0.038em; line-height: 1.02;
}
.present-divider.pd-section .pd-title { font-size: 3.6rem; max-width: 20ch; }
/* Chapter cover. Full selector, not `.present-divider.pd-chapter` — the
   base display rule above is more specific (body.presenting.…) and its
   `justify-content: center` was winning, clustering the cover's three
   groups mid-page instead of spreading top / middle / bottom. */
body.presenting.present-divider-on .present-divider.pd-chapter { justify-content: space-between; }
.present-divider .pd-covertop { display: flex; align-items: baseline; gap: 14px; }
.present-divider .pd-covercourse {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 1.05rem; font-weight: 500; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text2);
}
.present-divider .pd-covercode {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 0.85rem; color: var(--text3);
  border: 1px solid var(--border); border-radius: 4px; padding: 3px 9px;
}
.present-divider .pd-covermid { display: flex; align-items: center; gap: 36px; }
.present-divider .pd-covernum {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-weight: 500; font-size: 6.4rem; line-height: 0.9;   /* mockup 104px scaled */
  letter-spacing: -0.05em; color: var(--accent); flex: none;
}
.present-divider .pd-coverrule {
  width: 2px; align-self: stretch; background: var(--border); flex: none;
}
.present-divider .pd-coverlab {
  display: block;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 0.85rem; letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--text3); margin-bottom: 9px;
}
.present-divider .pd-byline {
  padding-top: 22px; border-top: 1px solid var(--border);
  display: flex; gap: 62px; flex-wrap: wrap;
}
/* No cells (signed-out deep link before the institution resolves): keep the
   element as the cover's bottom space-between anchor, drop its chrome. */
.present-divider .pd-byline:empty { border-top: none; padding-top: 0; }
.present-divider .pd-byline > div { display: flex; flex-direction: column; gap: 4px; }
.present-divider .pd-bylab {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 0.75rem; letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--text3);
}
.present-divider .pd-byval {
  font-family: var(--font-display, inherit);
  font-weight: 500; font-size: 1.25rem; letter-spacing: -0.01em; color: var(--text);
}
.present-divider .pd-bysub {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 0.85rem; color: var(--text2);
}
/* Section opening */
.present-divider .pd-kind {
  display: inline-block; align-self: flex-start;
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 0.9rem; font-weight: 500; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--accent);
  border: 1px solid var(--border); border-radius: 4px;
  padding: 4px 11px; margin-bottom: 22px;
}
.present-divider .pd-subtitle {
  font-size: 1.45rem; line-height: 1.52; color: var(--text2);
  margin: 24px 0 0; max-width: 54ch;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Numbered chip rows: the cover agenda + the opening contents. */
.present-divider .pd-subs {
  display: flex; flex-wrap: wrap; gap: 14px;
  margin-top: 30px;
  max-height: 168px; overflow: hidden;   /* belt — chips wrap, never overflow */
}
.present-divider .pd-sub-chip {
  font-family: var(--font-mono, 'Consolas', monospace);
  font-size: 1rem; color: var(--text2);
  border: 1px solid var(--border); border-radius: 4px;
  padding: 7px 13px; white-space: nowrap;
  max-width: 34ch; overflow: hidden; text-overflow: ellipsis;
}
.present-divider .pd-sub-chip i {
  font-style: normal; color: var(--accent); margin-inline-end: 10px;
}

/* THE STAGE: the design canvas, centred in the viewport and scaled to fit
   (`contain` — the whole slide is always visible, letterboxed in --bg when
   the screen aspect isn't the canvas's). Nothing is reparented to build it:
   .chapter-body already wraps <main>, and every one of its other children
   (lesson header, topbar, pager) is display:none while presenting. Slides
   stay visibility windows over the live DOM. */
body.presenting .chapter-body {
  position: fixed;
  top: 50%; inset-inline-start: 50%;
  width: var(--present-canvas-w);
  height: var(--present-canvas-h);
  /* RTL: the translate is physical — a dir="rtl" pass must flip its X sign
     (same caveat as .present-toolbar below). .present-frame mirrors this
     whole box — keep the two in lockstep or the frame drifts off the page. */
  transform: translate(-50%, -50%) scale(var(--present-fit, 1));
  transform-origin: center center;
  overflow: hidden;
  padding: 0; margin: 0;
}
/* THE PAGE: the white slide panel, painted as a FIXED rectangle on the stage
   — exactly where a panel sits (--present-col-w wide, --present-min-h tall,
   --present-pad-y down from the canvas top), so it is identical on every
   slide by construction.

   LOAD-BEARING that this is a pseudo-element and not the `.section` box
   (2026-07-29): the section carries the per-slide shrink below, and a `scale`
   is uniform in both axes, so while the section painted the page a shrunk
   slide drew its white card at s× a fitting slide's width AND height and the
   page visibly jumped from slide to slide. Painting it here instead costs
   ZERO layout, which is the point — every measurement the mode depends on
   (geometry()'s framePad, the paginated wrap widths, the ResizeObserver on
   #chapterMain that drives runtime-overflow scroll, and that scroll's height
   baseline) stays bit-identical to the pre-split behaviour. Moving the panel
   onto `main` instead would have been tidier structurally but would have had
   to pin the section's layout height to stop a visual-only shrink from
   stretching the page back out — and a pinned section stops changing main's
   content box, which silences that ResizeObserver on precisely the shrunk
   slides most likely to hold a `tryIt` reveal.

   `.chapter-body` is `position:fixed` (so it is the containing block) and
   `main` is `position:relative; z-index:1`, so content stacks above this
   with no extra rule. `inset-inline: 0` + a definite width + `margin-inline:
   auto` centres it on the same axis as `main`, and stays RTL-correct. */
body.presenting .chapter-body::before {
  content: '';
  position: absolute;
  top: var(--present-pad-y);
  inset-inline: 0;
  width: var(--present-col-w);
  height: var(--present-min-h);
  margin-inline: auto;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 10px 40px rgba(15, 20, 32, 0.06);
}
/* The content column, in design px. Padding + the panel's min-height add up
   to exactly the canvas height. Both selectors: the reading layout's
   `.chapter-layout.has-nav main` (max-width + side padding) out-specifies a
   bare main override. */
body.presenting main,
body.presenting .chapter-layout.has-nav main {
  width: var(--present-col-w);
  max-width: none;
  padding: var(--present-pad-y) 0 var(--present-reserve);
  margin: 0 auto;
}
/* PowerPoint feel: each slide is ONE white panel — the content-card
   language at projector scale — vertically centering short content
   (min-height only, full slides start at top). The measuring state gets
   the same padding/border so wrap widths and the height budget match;
   geometry() subtracts the panel frame (pad-y + pad-b + borders) from the
   per-slide budget it publishes as --present-avail-h. The BOX lives here;
   only the paint moved to the stage rectangle above. */
body.presenting .section.active,
body.presenting .section.present-measuring {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  /* Vertical padding also clears the v3 frame (progress + band on top,
     foot below — measureAndPaginate nets the same heights out of the
     budget). Shared with the measuring state, so wrap widths (horizontal,
     unchanged) and the budget can never drift. */
  padding: calc(var(--present-panel-pad-y) + var(--present-prog-h, 4px) + var(--present-band-h, 56px))
           var(--present-panel-pad-x)
           calc(var(--present-panel-pad-b) + var(--present-foot-h, 50px));
  box-shadow: 0 10px 40px rgba(15, 20, 32, 0.06);
}
body.presenting .section.active {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: var(--present-min-h);
}
/* Normally the stage rectangle IS the page, so the section paints nothing —
   its border stays 1px and merely turns transparent, so not one measured
   length moves. It only paints its own card again when the slide has really
   outgrown the page (`present-overflowing`), and at that moment the shrink
   has already been dropped (applyScrollScale) so the section is unscaled,
   full width, and must grow its background with the scrolling content —
   which is exactly what it did before the split. The fixed rectangle steps
   aside then, since an absolutely-positioned pseudo-element would scroll
   away from the content it is meant to sit behind. */
body.presenting:not(.present-overflowing) .section.active {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}
body.presenting.present-overflowing .chapter-body::before { display: none; }
/* Runtime overflow: when a reveal opens (or quiz feedback appears) and the
   slide grows past the fixed canvas, _presentMode.js sets `present-overflowing`
   and scrolls the stage. Top-align the panel then — a centered panel taller
   than the canvas would push its own TOP above the scroll origin, out of reach.
   Normal (fitting) slides keep `justify-content: center`. */
body.presenting.present-overflowing .section.active { justify-content: flex-start; }
/* Projector type rules are deliberately minimal: the stage transform scales
   every component (text, kickers, buttons, code, badges) uniformly, so
   reading sizes ARE the slide sizes. The exceptions are structural, not
   decorative: the injected .present-title and the (subtitle-only) section
   header sit outside the per-slide fit factor, so the slide title is one
   size on every slide; in-content headings get a pinned rem base further
   down (they still ride the content fit, capped at MAX_UPSCALE /
   MIN_SLIDE_SCALE); and code pins to a projector-legible size
   (--code-font-size 18px design ≈ 24px projected — see the frozen-canvas
   block below). */

/* ── Frozen slide canvas (body.slide-layout) ─────────────────
   Set by BOTH surfaces that lay content out on the fixed canvas: Present
   mode (alongside .presenting) and the chapter editor's Slide view (in the
   canvas iframe). Narrow-viewport rules elsewhere in this file opt out of
   it — a rehearsed slide must render the same in a small window. */

/* Per-slide fit — ONE zoom factor per flow slide since 2026-07-30
   (--present-fill, minScale..MAX_UPSCALE, from _presentMode.js measureFit,
   measured under the same zoom off-screen so measurement == display). It
   lives on the section's .blocks-flow, never the section, so the injected
   slide title and the section header keep ONE size on every slide.
   `zoom` in BOTH directions, deliberately not `scale`:
   - magnifying (f > 1): a scale that kept the measured wrap width would
     overflow the page sideways; zoom re-wraps at width/f and magnifies back
     to the full column. Capped by the widest non-wrapping descendant (a
     long code line) so it can never mint a horizontal scrollbar.
   - shrinking (f < 1): a scale kept the measured wrap width and rendered
     the slide centered between dead side gutters (the "narrow card in the
     middle 60%" complaint); zoom re-wraps WIDER — width/f > colW — so a
     dense slide keeps the full column and needs less shrink at all.
   Being LAYOUT-affecting is also why no height pin is ever needed and why
   runtime growth (reveals, quiz feedback) keeps layout == visual. (`zoom`
   is Baseline 2024; an engine without it renders 1:1 — never broken.)

   The quiz pseudo-section has no .blocks-flow: it keeps the old
   whole-section transform shrink (second rule; --present-slide-scale +
   .present-shrunk written by setSlideScale — the class is still the pin
   the editor deck's real page box needs, see _presentMode.js). For flow
   sections the vars sit inline on the FLOW, so the section rule reads its
   defaults and stays inert. */
body.slide-layout .section.active:not(.present-measuring) .blocks-flow {
  zoom: var(--present-fill, 1);
}
body.slide-layout .section.active:not(.present-measuring) {
  scale: var(--present-slide-scale, 1);
  transform-origin: top center;
}
/* The one viewport-relative type rule in this file (h1's 3vw term). */
body.slide-layout h1 { font-size: 2.125rem; }
/* Pinned rem bases for in-content headings on the frozen canvas: on a slide
   they are subtitles, and an em size would let a font-size context drift
   them between slides. (The card-kicker restyle out-specifies this on a
   card-leading h3, exactly as in reading mode.) */
body.slide-layout .section .blocks-flow h2 { font-size: 1.8rem; }
body.slide-layout .section .blocks-flow h3 { font-size: 1.6rem; }
body.slide-layout .section .blocks-flow h4 { font-size: 1.3rem; }
/* Projector code size (2026-07-30): reading-size code (14px design ≈ 19px
   projected at 1080p) is unreadable from the back row — the frozen canvas
   pins it to 18px design ≈ 24px projected (compact 15px ≈ 20px). Scoped on
   body, so it beats theme-loader's :root inline vars for slide surfaces
   only; pagination and the zoom fit measure under the override, so budgets
   stay exact. */
body.slide-layout {
  --code-font-size: 18px;
  --code-font-size-compact: 15px;
}
/* (Slide typography deliberately rides the PLATFORM --font-* tokens — a
   brief 2026-07-30 experiment scoping the v3 mockup's faces here was
   reverted the same day at the user's request; only the mockup's sizes,
   weights and letter-spacing survive.) */
/* Migrated html-block code often carries its old ad-hoc INLINE font-size
   inside the payload (e.g. style="font-size:14px" from the static-chapter
   era), which no var can reach — on the projector the legibility floor
   wins over content styling, and !important is required precisely to beat
   those inline declarations. Compact keeps its denser size (rule order
   breaks the specificity tie). Reading mode is untouched. */
body.slide-layout .code-block { font-size: var(--code-font-size, 18px) !important; }
body.slide-layout .code-block--compact { font-size: var(--code-font-size-compact, 15px) !important; }

/* Pagination plumbing (applied by _presentMode.js). */
.present-measuring {
  display: block !important;
  position: absolute !important; inset-inline-start: -9999px; top: 0;
  visibility: hidden;
  width: var(--present-col-w, 1308px);
}
.present-hidden { display: none !important; }
/* The in-content element the slide's leaf title was lifted from (a leading
   heading block or a card title) — hidden per slide by applySlideVisibility
   so the topic isn't printed twice under the injected .present-title. */
body.presenting .present-dup-title { display: none !important; }
/* Only set when even the MIN_SLIDE_SCALE fit can't hold the block; the
   budget is expressed inside the slide's own fit coordinate space (zoom for
   flow sections, transform scale for the quiz fallback — the unused var of
   the two divides by its default 1).
   --present-avail-c is the CURRENT slide's content share of the budget (net
   of the unscaled header on a header slide), written inline on the section
   by applySlideVisibility and inherited down to the unit; the editor's Slide
   view never writes it and falls back to the whole budget as before. */
.present-slide-overflow {
  max-height: calc(var(--present-avail-c, var(--present-avail-h, 634px)) / var(--present-slide-scale, 1) / var(--present-fill, 1));
  overflow-y: auto;
}

/* Ink canvas + text-notes overlay. pointer-events stay OFF until a
   draw tool is armed, so quiz options / try-it buttons stay clickable. */
/* LOAD-BEARING `width/height:100%` on both overlay canvases (2026-07-23).
   A <canvas> is a REPLACED element, so `inset: 0` does NOT stretch it — the
   intrinsic size from its width/height ATTRIBUTES wins and `right`/`bottom`
   are ignored. Since _presentMode.js sizes those attributes in DEVICE px
   (css × devicePixelRatio), the element was rendered dpr× too large while its
   2D context was scaled by dpr, so ink/highlighter/laser landed at dpr× the
   cursor position — pixel-perfect at the top-left, drifting toward the
   bottom-right, and invisible at dpr 1 (which is why it survived review).
   Do not "simplify" these away. */
/* The overlays also declare every geometry property they DON'T want, not just
   the ones they do (2026-07-30). Chapter-provided CSS lands in the same
   document, and any property the overlay leaves undeclared is a property a
   chapter rule wins by default: the cos-102 OOP chapters ship
   `canvas { width:100%; max-width:720px }` for their UML diagrams, and the
   unopposed `max-width` clamped this full-viewport annotation layer to 720px
   — drawing died on the rest of the screen. Chapter CSS is now scoped at
   injection (_dynamicChapterRender.js), so this is belt-and-braces; keep both. */
.present-ink,
canvas.present-laser {
  max-width: none; max-height: none; min-width: 0; min-height: 0;
  margin: 0; padding: 0; border: 0; border-radius: 0;
  transform: none; clip-path: none; filter: none;
  box-sizing: border-box;
}
.present-ink {
  position: fixed; inset: 0; z-index: 8500;
  width: 100%; height: 100%;
  pointer-events: none; touch-action: none;
}
.present-ink.armed { pointer-events: auto; cursor: crosshair; }
/* While a draw tool is armed, a drag must never start a text selection —
   even if an event slips past the armed canvas. The Interact tool keeps
   selection on purpose (single click is reserved for select/copy). */
body.present-tool-armed { user-select: none; -webkit-user-select: none; }
/* Laser pointer layer. Never armed, never interactive — the dot must never
   be able to block the click it is pointing at.
   The `canvas` qualifier is LOAD-BEARING: the body-level state class below is
   a DIFFERENT name for exactly this reason. An unqualified `.present-laser`
   also matched <body> while the laser was on, and since `pointer-events` is an
   INHERITED property that made the whole page — toolbar included — unclickable,
   so the laser could never be switched off again. Keep layer classes and
   body-state classes disjoint. */
canvas.present-laser {
  position: fixed; inset: 0; z-index: 8700;
  width: 100%; height: 100%;   /* replaced-element trap — see .present-ink */
  pointer-events: none;
}
/* Laser on: the glowing dot REPLACES the arrow (real presenter-remote feel),
   except over the chrome, which still needs an aimable cursor. */
body.present-laser-on, body.present-laser-on * { cursor: none; }
body.present-laser-on .present-toolbar,
body.present-laser-on .present-toolbar *,
body.present-laser-on .present-nav { cursor: pointer; }
.present-notes { position: fixed; inset: 0; z-index: 8600; pointer-events: none; }
.present-notes.armed { pointer-events: auto; cursor: text; }
.present-note {
  position: absolute; pointer-events: auto;
  min-width: 60px; max-width: 420px;
  padding: 8px 12px; border: 2px solid var(--warn); border-radius: 8px;
  background: rgba(var(--warn-rgb), 0.12); color: var(--text);
  font-size: 1.15rem; line-height: 1.45; outline: none;
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}

/* Edge prev/next buttons + slide counter. */
.present-nav {
  position: fixed; top: 50%; transform: translateY(-50%); z-index: 8800;
  width: 52px; height: 84px;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--border); background: var(--card); color: var(--text3);
  font-size: 24px; cursor: pointer; opacity: 0.35; transition: opacity 0.2s;
}
.present-nav:hover { opacity: 1; color: var(--accent); border-color: var(--accent); }
.present-nav.prev { inset-inline-start: 0; border-inline-start: none; border-start-end-radius: 10px; border-end-end-radius: 10px; }
.present-nav.next { inset-inline-end: 0; border-inline-end: none; border-start-start-radius: 10px; border-end-start-radius: 10px; }
.present-counter {
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 13px; color: var(--text3);
  white-space: nowrap; padding: 0 10px;
}

/* ── v3 slide frame: page progress · band · foot (2026-07-30) ──
   Replaces the floating counter pill and the viewport-top hairline. The
   three elements are PAGE FURNITURE pinned to the canvas — aligned to the
   painted page rectangle, scaling with --present-fit, present in any
   capture, never idle-hidden — and the content scrolls UNDER them
   (--card backgrounds, so it passes beneath cleanly). Heights are the
   --present-prog/band/foot-h vars netted out of the slide budget.
   Injected by _presentMode.js buildFrame, content per slide by
   renderFrame(), removed with the overlay.

   THEY ARE NOT STAGE CHILDREN (fixed 2026-07-31). They used to be, and
   that was the bug: the stage IS the scroll container (see the
   runtime-overflow block above — .chapter-body keeps overflow:hidden and
   _presentMode.js drives its scrollTop), and an absolutely-positioned
   child of a scroll container translates with scrollTop. So opening a
   `tryIt` reveal mid-presentation both grew the section past the foot's
   constant offset AND dragged the foot upward with the auto-scroll —
   the foot ended up painted across the middle of the slide. Those are
   one failure, not two: `main`'s content box IS the page rect by
   construction (padding pad-y / reserve, and minH = canvasH − reserve −
   pad-y), so content can leave the page rect only once the stage is
   scrollable.

   They now ride .present-frame below — a body-level layer that mirrors
   the stage's box declaration-for-declaration, so every offset here
   resolves against an identical rectangle and nothing else had to move
   (not the scroll engine, not pagination, not the growth baselines, not
   the ResizeObserver). Follow-up if the mode is ever reworked: re-home
   that RO onto the active .section and `main` can take a pinned height +
   overflow:hidden, at which point this whole layer and its masks
   collapse into the stage no longer scrolling at all. */
.present-frame {
  position: fixed;
  top: 50%; inset-inline-start: 50%;
  width: var(--present-canvas-w);
  height: var(--present-canvas-h);
  /* RTL: physical translate — a dir="rtl" pass must flip its X sign. Same
     caveat as body.presenting .chapter-body above; it now applies to BOTH
     rules, and they must stay identical or the frame drifts off the page. */
  transform: translate(-50%, -50%) scale(var(--present-fit, 1));
  transform-origin: center center;
  overflow: hidden;
  pointer-events: none;
  z-index: 8400;
  /* No border/padding, deliberately: * { box-sizing: border-box } makes both
     this and the stage border-boxes, so adding either here would shrink the
     content box and desync the two rectangles. No JS keeps them in step —
     neither <html> nor <body> is transformed while presenting, so both
     resolve top/left:50% against the same containing block, and both read
     the same --present-canvas-w/-h + --present-fit off <body> (one
     --present-fit write updates both in a single recalc). .present-ink
     already relies on body-level position:fixed sharing the stage's
     coordinate space. */
}
/* Gutter masks. The page rectangle is [pad-y, pad-y + min-h]; above and below
   it the stage shows bare letterbox. Content used to scroll with the frame,
   so nothing was ever visible there — now the frame holds still and scrolled
   content passes straight through both gutters, so cover them.
   --lesson-content-bg, NOT --bg: the stage INTERIOR is .chapter-body's own
   background (see the base rule near the top of this file, never overridden
   by body.presenting), while --bg is the letterbox OUTSIDE the stage.
   Only while overflowing, so a normal slide is byte-identical to before —
   safe because, per the note above, content cannot leave the page rect
   unless the stage is scrollable. And no z-index on purpose: they must paint
   below the z-index:2 bars, which is what hides the deliberate ±1px overlap
   (pad-y and pad-y + min-h are fractional after theming and again after the
   --present-fit scale, and an exactly-abutting edge can leave a sub-pixel
   seam for content to bleed through). */
body.presenting.present-overflowing .present-frame > .pfr-mask-top,
body.presenting.present-overflowing .present-frame > .pfr-mask-bot {
  position: absolute; inset-inline: 0;
  background: var(--lesson-content-bg, #f6f7f9);
}
body.presenting.present-overflowing .present-frame > .pfr-mask-top {
  top: 0; height: calc(var(--present-pad-y) + 1px);
}
body.presenting.present-overflowing .present-frame > .pfr-mask-bot {
  top: calc(var(--present-pad-y) + var(--present-min-h) - 1px); bottom: 0;
}
.present-pageprog, .present-band, .present-foot {
  position: absolute; inset-inline: 0; margin-inline: auto;
  width: var(--present-col-w);
  z-index: 2; pointer-events: none;
  background: var(--card);
}
.present-pageprog {
  top: var(--present-pad-y);
  height: var(--present-prog-h, 4px);
  background: var(--bg3, var(--border));
  border-start-start-radius: 14px; border-start-end-radius: 14px;
  overflow: hidden;
}
.present-pageprog .ppg-fill {
  height: 100%; width: 0;
  background: var(--accent);
  transition: width 0.25s ease;
}
.present-band {
  top: calc(var(--present-pad-y) + var(--present-prog-h, 4px));
  height: var(--present-band-h, 56px);
  display: flex; align-items: center; gap: 16px;
  padding: 0 var(--present-panel-pad-x);
  border-bottom: 1px solid var(--border);
  font-family: var(--font-mono, 'Consolas', monospace);
}
.present-band .pb-where {
  flex: 1; display: flex; align-items: center; gap: 9px;
  font-size: 14px; color: var(--text3); min-width: 0;
  white-space: nowrap; overflow: hidden;
}
.present-band .pb-where b { color: var(--text); font-weight: 500; }
.present-band .pb-where span, .present-band .pb-where b {
  overflow: hidden; text-overflow: ellipsis;
}
.present-band .pb-of {
  font-size: 13px; color: var(--text3);
  border: 1px solid var(--border); border-radius: 3px; padding: 2px 8px;
  white-space: nowrap;
}
.present-band .pb-beats { display: flex; gap: 5px; }
.present-band .pb-beat {
  width: 24px; height: 4px; border-radius: 2px;
  background: var(--bg3, var(--border));
}
.present-band .pb-beat.on { background: var(--accent); }
.present-foot {
  top: calc(var(--present-pad-y) + var(--present-min-h) - var(--present-foot-h, 50px));
  height: var(--present-foot-h, 50px);
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 var(--present-panel-pad-x);
  border-top: 1px solid var(--border);
  border-end-start-radius: 14px; border-end-end-radius: 14px;
  font-family: var(--font-mono, 'Consolas', monospace);
}
.present-foot .pf-sig { display: flex; align-items: center; gap: 10px; min-width: 0; }
.present-foot .pf-sig .axlogo-lockup { flex: none; }
.present-foot .pf-mark {
  width: 20px; height: 20px; border-radius: 5px; background: var(--accent);
  display: grid; place-items: center; color: #fff;
  font-family: var(--font-display, inherit); font-weight: 700; font-size: 12px;
}
.present-foot .pf-name {
  font-family: var(--font-display, inherit); font-weight: 700; font-size: 15px;
  letter-spacing: -0.015em; color: var(--text);
}
.present-foot .pf-sigdiv { width: 1px; height: 15px; background: var(--border); flex: none; }
.present-foot .pf-inst {
  font-size: 13px; color: var(--text2); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.present-foot .pf-slug { font-size: 13px; color: var(--text3); white-space: nowrap; }
.present-foot .pf-count { font-size: 13px; color: var(--text2); white-space: nowrap; }

/* ── Outline jump menu ───────────────────────────────────────
   Opened from the ☰ toolbar button or the O key; rebuilt from the slide
   list on every open (repagination-proof). One row per section and per
   subsection heading, numbered with its first slide; the row covering the
   current slide is highlighted. z 8950: above the edge arrows, below the
   toolbar pill it pops up from. */
.present-outline {
  position: fixed; bottom: 78px; inset-inline-start: 50%; transform: translateX(-50%);
  z-index: 8950;
  display: none;
  width: min(560px, 92vw); max-height: min(62vh, 640px);
  flex-direction: column;
  border: 2px solid var(--border); border-radius: 16px;
  background: var(--card);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.22);
  overflow: hidden;
}
.present-outline.open { display: flex; }
.present-outline .po-title {
  padding: 12px 18px 8px;
  font-size: 11px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--text3);
}
.present-outline .po-list { overflow-y: auto; padding: 0 8px 10px; }
.present-outline .po-row {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 8px 12px; border: 0; border-radius: 8px;
  background: none; color: var(--text2); text-align: start;
  font-family: inherit; font-size: 14px; cursor: pointer;
}
.present-outline .po-row:hover { background: var(--bg3); color: var(--text); }
.present-outline .po-row.po-sec { font-weight: 700; color: var(--text); margin-top: 4px; }
.present-outline .po-row.po-sub { padding-inline-start: 34px; }
.present-outline .po-row.current { background: rgba(var(--accent-rgb), 0.1); color: var(--accent); }
.present-outline .po-num {
  flex: 0 0 auto; min-width: 26px; text-align: center;
  font-family: var(--font-mono, 'Consolas', monospace); font-size: 11px;
  color: var(--text3); border: 1px solid var(--border); border-radius: 6px;
  padding: 1px 4px;
}
.present-outline .po-row.current .po-num { color: var(--accent); border-color: var(--accent); }
.present-outline .po-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Bottom-center toolbar pill.
   `width: max-content` is LOAD-BEARING: a fixed element positioned at
   `inset-inline-start: 50%` shrink-to-fits against only the REMAINING half
   of the viewport, so without an explicit width the pill wraps into rows
   long before `max-width` ever binds (it did, at ≤2× its natural width). */
.present-toolbar {
  position: fixed; bottom: 16px; inset-inline-start: 50%; transform: translateX(-50%);
  z-index: 9000;
  display: flex; align-items: center; gap: 4px;
  width: max-content;
  padding: 8px 12px; border: 2px solid var(--border); border-radius: 40px;
  background: var(--card); box-shadow: 0 8px 30px rgba(0,0,0,0.18);
  max-width: 96vw; flex-wrap: wrap; justify-content: center;
  transition: opacity 0.2s;   /* idle auto-hide */
}
.pt-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 38px; height: 38px; padding: 0 8px;
  border: 2px solid transparent; border-radius: 20px;
  background: none; color: var(--text2);
  font-family: inherit; font-size: 16px; cursor: pointer;
  transition: all 0.15s;
}
.pt-btn:hover { background: var(--bg3); color: var(--text); }
.pt-btn:disabled { opacity: 0.35; cursor: default; pointer-events: none; }
.pt-btn.active {
  border-color: var(--accent);
  background: rgba(var(--accent-rgb), 0.1); color: var(--accent);
}
.pt-btn.pulse { animation: presentPulse 1.4s ease-in-out infinite; }
@keyframes presentPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.5); }
  50%      { box-shadow: 0 0 0 8px rgba(var(--accent-rgb), 0); }
}
.pt-sep { width: 1px; align-self: stretch; margin: 4px 2px; background: var(--border); }
.pt-swatch {
  width: 24px; height: 24px; border-radius: 50%;
  border: 2px solid var(--border); cursor: pointer; padding: 0;
  transition: transform 0.15s;
}
.pt-swatch:hover { transform: scale(1.15); }
.pt-swatch.active { border-color: var(--text); box-shadow: 0 0 0 2px var(--card), 0 0 0 4px var(--text3); }

/* ── Idle auto-hide ──────────────────────────────────────────
   ~3s without pointer or key activity and the room sees only the slide:
   cursor, toolbar and edge arrows fade out; any activity brings them back.
   _presentMode.js suppresses the class entirely while a draw tool is armed,
   while a dialog is open, or while the pointer rests on the toolbar. */
body.presenting.present-idle,
body.presenting.present-idle * { cursor: none; }
body.presenting.present-idle .present-toolbar,
body.presenting.present-idle .present-nav,
body.presenting.present-idle .present-scroll-hint {
  opacity: 0;
  pointer-events: none;
}

/* ── "More below" scroll cue ─────────────────────────────────
   Shown by _presentMode.js (`.on`) only while an expanded slide can still
   scroll DOWN — a soft fade off the viewport bottom + a gently bouncing
   chevron, so the room knows there is more. Fixed to the viewport (not the
   scrolling stage), pointer-events off, and it sits BELOW the toolbar so the
   controls stay reachable. Fades out with the rest of the chrome when idle. */
.present-scroll-hint {
  position: fixed; inset-inline: 0; bottom: 0; z-index: 8850;
  height: 96px; pointer-events: none;
  display: flex; align-items: flex-end; justify-content: center;
  padding-bottom: 74px;   /* clear the bottom-center toolbar pill */
  opacity: 0; transition: opacity 0.25s;
  /* Fade to a TRANSPARENT version of --bg (not the keyword `transparent`, which
     interpolates through muddy black); theme-adaptive, token-driven. */
  background: linear-gradient(to top, var(--bg), color-mix(in srgb, var(--bg), transparent));
}
.present-scroll-hint.on { opacity: 1; }
.present-scroll-hint .psh-chevron {
  font-size: 26px; line-height: 1; color: var(--accent);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.25));
  animation: pshBounce 1.5s ease-in-out infinite;
}
@keyframes pshBounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(5px); }
}

/* Blackout ('B'). Below the native top layer on purpose: an open
   try-it dialog stays visible above a blacked-out backdrop. */
.present-blackout { position: fixed; inset: 0; z-index: 9500; background: #000; display: none; }
.present-blackout.on { display: block; }

/* ═══ Lecture recording + replay chrome (2026-07-30) ═══════════
   Mode chrome, not learning content — same classification as the present
   toolbar above. _lectureRecorder.js (staff capture, `rec-*`) and
   _lectureReplay.js (student playback, `replay-*` / `rp-*` / `rl-*`). */

/* Recorder button lives inside the present toolbar pill (a .pt-btn). The
   dot doubles as a live mic meter: --rec-level (0..1) is written per frame
   by the recorder's analyser. */
.rec-btn .rec-dot {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--err, #ef4444);
  box-shadow: 0 0 0 calc(6px * var(--rec-level, 0)) rgba(239, 68, 68, 0.35);
  transition: box-shadow 0.1s;
}
.rec-btn.recording .rec-dot { animation: recBlink 1.2s ease-in-out infinite; }
.rec-btn.paused .rec-dot { background: var(--warn, #f59e0b); animation: none; }
@keyframes recBlink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}
.rec-time {
  font-family: var(--font-mono, monospace); font-size: 13px;
  color: var(--err, #ef4444); min-width: 42px; text-align: center;
}

/* Microphone picker popover (🎙 button beside Rec; idle state only). */
.rec-mic-menu {
  position: fixed; bottom: 74px; inset-inline-start: 50%;
  transform: translateX(-50%); z-index: 9100;
  min-width: 260px; max-width: min(420px, 92vw);
  padding: 8px; border: 2px solid var(--border); border-radius: 12px;
  background: var(--card); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
  display: flex; flex-direction: column; gap: 2px;
}
.rec-mic-head {
  padding: 4px 10px 6px; font-size: 11.5px; font-weight: 700;
  letter-spacing: 0.6px; text-transform: uppercase; color: var(--text3);
}
.rec-mic-row {
  display: block; width: 100%; padding: 8px 12px;
  border: 2px solid transparent; border-radius: 8px;
  background: none; color: var(--text2); font-family: inherit;
  font-size: 13.5px; text-align: start; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rec-mic-row:hover { background: var(--bg3); color: var(--text); }
.rec-mic-row.on {
  border-color: var(--accent); color: var(--accent);
  background: rgba(var(--accent-rgb), 0.08);
}
.rec-mic-row.on::after { content: " \2713"; }

/* Save / discard dialog — a plain overlay (NOT <dialog>: it must survive
   the present-mode teardown when the instructor exits mid-recording). */
.rec-save {
  position: fixed; inset: 0; z-index: 9700;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.5);
}
.rec-save-box {
  width: min(440px, 92vw); padding: 24px;
  border: 2px solid var(--border); border-radius: 14px;
  background: var(--card); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  display: flex; flex-direction: column; gap: 14px;
}
.rec-save-title { font-weight: 700; font-size: 18px; color: var(--text); }
.rec-save-info { font-size: 13px; color: var(--text3); }
.rec-save-name {
  padding: 10px 12px; border: 2px solid var(--border); border-radius: 8px;
  background: var(--bg2); color: var(--text);
  font-family: inherit; font-size: 15px;
}
.rec-save-name:focus { outline: none; border-color: var(--accent); }
.rec-save-row { display: flex; justify-content: flex-end; gap: 10px; }
.rec-save-ok, .rec-save-discard {
  padding: 9px 18px; border-radius: 8px; cursor: pointer;
  font-family: inherit; font-size: 14px; font-weight: 600;
  transition: all 0.15s;
}
.rec-save-ok {
  border: 2px solid var(--accent); background: var(--accent); color: #fff;
}
.rec-save-ok:hover { filter: brightness(1.08); }
.rec-save-discard {
  border: 2px solid var(--border); background: none; color: var(--text2);
}
.rec-save-discard:hover { border-color: var(--err, #ef4444); color: var(--err, #ef4444); }
.rec-save-ok:disabled, .rec-save-discard:disabled { opacity: 0.5; cursor: default; }

/* Transient toast (recorder + player share it). */
.rec-toast {
  position: fixed; top: 18px; inset-inline-start: 50%;
  transform: translate(-50%, -12px); z-index: 9800;
  max-width: min(480px, 90vw); padding: 10px 18px;
  border: 2px solid var(--border); border-radius: 10px;
  background: var(--card); color: var(--text);
  font-size: 14px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
  opacity: 0; transition: opacity 0.3s, transform 0.3s;
  pointer-events: none;
}
.rec-toast.on { opacity: 1; transform: translate(-50%, 0); }

/* Student chip in the chapter header — flows under the title (the staff
   present-launch button owns the absolute top-right corner). */
.replay-launch {
  display: inline-flex; align-items: center; gap: 8px; margin-top: 12px;
  padding: 7px 14px; border: 2px solid var(--border); border-radius: 20px;
  background: var(--card); color: var(--text2);
  font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  transition: all 0.2s;
}
.replay-launch:hover {
  border-color: var(--accent); color: var(--accent);
  box-shadow: 0 4px 12px rgba(var(--accent-rgb), 0.15);
}
body.presenting .replay-launch, body.replaying .replay-launch { display: none !important; }

/* Replay mode: the deck machinery runs but the presenter chrome stands
   down — the player bar below is the only control surface. */
body.replaying .present-toolbar,
body.replaying .present-nav,
body.replaying .present-scroll-hint { display: none !important; }
body.replaying .present-notes { pointer-events: none; }

/* Player bar — same pill language as the present toolbar. */
.replay-bar {
  position: fixed; bottom: 16px; inset-inline-start: 50%; transform: translateX(-50%);
  z-index: 9000;
  display: flex; align-items: center; gap: 10px;
  width: max-content; max-width: 96vw;
  padding: 8px 14px; border: 2px solid var(--border); border-radius: 40px;
  background: var(--card); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
}
.rp-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 38px; height: 38px; padding: 0 10px;
  border: 2px solid transparent; border-radius: 20px;
  background: none; color: var(--text2);
  font-family: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
  transition: all 0.15s;
}
.rp-btn:hover { background: var(--bg3); color: var(--text); }
.rp-btn.active {
  border-color: var(--accent);
  background: rgba(var(--accent-rgb), 0.1); color: var(--accent);
}
.rp-play { font-size: 17px; }
.rp-time {
  font-family: var(--font-mono, monospace); font-size: 13px;
  color: var(--text2); white-space: nowrap;
}
.rp-scrub { width: min(440px, 42vw); accent-color: var(--accent); cursor: pointer; }
.rp-voice { font-size: 12px; }
.rp-exit:hover { border-color: var(--err, #ef4444); color: var(--err, #ef4444); }

/* Recording list overlay. */
.replay-list {
  position: fixed; inset: 0; z-index: 9600;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.45);
}
.replay-list-box {
  position: relative; width: min(520px, 92vw); max-height: 76vh;
  display: flex; flex-direction: column;
  border: 2px solid var(--border); border-radius: 14px;
  background: var(--card); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  overflow: hidden;
}
.rl-title {
  padding: 18px 22px 12px; font-size: 18px; font-weight: 700; color: var(--text);
}
.rl-close {
  position: absolute; top: 14px; inset-inline-end: 14px;
  width: 32px; height: 32px; border: none; border-radius: 8px;
  background: none; color: var(--text3); font-size: 15px; cursor: pointer;
}
.rl-close:hover { background: var(--bg3); color: var(--text); }
.rl-body { overflow-y: auto; padding: 0 12px 14px; }
.rl-empty { padding: 20px 10px; text-align: center; color: var(--text3); font-size: 14px; }
.rl-row {
  display: flex; align-items: center; gap: 6px;
  border-radius: 10px; padding: 2px;
}
.rl-row:hover { background: var(--bg2); }
.rl-row.busy { opacity: 0.55; pointer-events: none; }
.rl-main {
  flex: 1; display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 10px 12px; border: none; border-radius: 8px;
  background: none; cursor: pointer; text-align: start; font-family: inherit;
}
.rl-name { font-size: 15px; font-weight: 600; color: var(--text); }
.rl-meta { font-size: 12.5px; color: var(--text3); }
.rl-act {
  min-width: 34px; height: 34px; border: 2px solid transparent; border-radius: 8px;
  background: none; color: var(--text3); font-size: 14px; cursor: pointer;
  font-family: inherit;
}
.rl-act:hover { background: var(--bg3); color: var(--text); }
.rl-act:disabled { opacity: 0.5; cursor: default; }
.rl-del:hover { color: var(--err, #ef4444); }

/* ── Captions + transcript (2026-07-30, lecture transcription) ── */
/* Caption line: fixed above the player pill, never intercepts clicks. */
.rp-captions {
  position: fixed; bottom: 84px; inset-inline-start: 50%;
  transform: translateX(-50%); z-index: 8990;
  max-width: min(70ch, 88vw); padding: 8px 18px;
  border-radius: 10px; background: rgba(15, 20, 32, 0.82); color: #fff;
  font-size: 17px; line-height: 1.45; text-align: center;
  pointer-events: none; opacity: 0; transition: opacity 0.15s;
}
.rp-captions.on { opacity: 1; }

/* Transcript panel: right-side column over the stage, below the top chrome. */
.rp-transcript {
  position: fixed; top: 16px; inset-inline-end: 16px; bottom: 84px;
  width: min(360px, 88vw); z-index: 8995;
  display: none; flex-direction: column;
  border: 2px solid var(--border); border-radius: 14px;
  background: var(--card); box-shadow: 0 16px 50px rgba(0, 0, 0, 0.28);
  overflow: hidden;
}
.rp-transcript.open { display: flex; }
.rp-tr-head {
  padding: 12px 16px 8px; font-weight: 700; font-size: 15px; color: var(--text);
  border-bottom: 1px solid var(--border);
}
.rp-tr-body { overflow-y: auto; padding: 6px; }
.rp-tr-row {
  display: block; width: 100%; padding: 7px 10px;
  border: none; border-radius: 8px; background: none;
  font-family: inherit; font-size: 13.5px; line-height: 1.5;
  color: var(--text2); text-align: start; cursor: pointer;
}
.rp-tr-row:hover { background: var(--bg2); color: var(--text); }
.rp-tr-row.on {
  background: rgba(var(--accent-rgb), 0.1); color: var(--text);
}
.rp-tr-time {
  display: inline-block; min-width: 40px; margin-inline-end: 6px;
  font-family: var(--font-mono, monospace); font-size: 11.5px;
  color: var(--accent);
}
