/* ============================================================
   Lesson player — Graph Paper (warm paper, ink, one red pen)
   ============================================================
   The board is the same warm paper the shell pages are, ruled
   with the same faint 30px blue grid. Everything the tutor
   draws is dark slate ink with an alpha hierarchy; ONE red-pen
   accent does all the highlighting and annotating, and blue
   ballpoint carries quantities (bar fills, shaded grids) so
   red stays reserved for emphasis.

   Type: board title Newsreader 500 · equations Newsreader/KaTeX
   · board writing Public Sans on 0.9 ink · notes Newsreader
   italic · meta IBM Plex Mono caps · boxes 1.5px ink rule
   (radius 2); the annotation box is red rule + red wash.

   Structure, animations, timings, and JS hooks are unchanged —
   every selector the player JS reads is preserved, and sizes
   are untouched so compiled lessons lay out exactly as before.
   Only surfaces, colors, and typefaces changed.

   The JS-drawn SVG surfaces read the --chalk / --accent /
   --foil-* tokens below (getComputedStyle at init), so this
   file is the single source of truth for the board's palette.
   The --chalk* names are kept because the player JS reads them
   by name; they hold ink values now.
   ============================================================ */

/* --- Design tokens (Tweaks panel writes to these) --- */
.player-body {
    --board-surface: #FAF8F2;                     /* the paper */
    --board-ink:     rgba(34, 48, 58, 0.9);       /* board writing */
    --board-ink-2:   rgba(34, 48, 58, 0.7);
    --board-ink-3:   rgba(34, 48, 58, 0.55);
    --board-rule:    rgba(34, 48, 58, 0.22);
    --accent:        #C2402A;                     /* red pen — the one accent */
    --accent-soft:   rgba(194, 64, 42, 0.12);
    --ballpoint:     #2B5DA8;                     /* blue: quantities, data */

    /* The ruled grid, as one value so every paper panel (the page, the
       scratch pane) rules itself identically. */
    --paper-grid:
        linear-gradient(rgba(43, 93, 168, 0.065) 1px, transparent 1px),
        linear-gradient(90deg, rgba(43, 93, 168, 0.065) 1px, transparent 1px);

    /* Ink tokens the SVG surfaces read (lesson_player.js _boardTok). */
    --chalk:        #22303A;                      /* full-strength ink line */
    --chalk-soft:   rgba(34, 48, 58, 0.55);
    --chalk-rule:   rgba(34, 48, 58, 0.22);
    --chalk-box:    rgba(34, 48, 58, 0.75);       /* 1.5px rule boxes */
    --foil-first:   #C2402A;                      /* red pen */
    --foil-outer:   #2B5DA8;                      /* ballpoint blue */
    --foil-inner:   #2F7A55;                      /* deep sage */
    --foil-last:    #8A4A6B;                      /* deep plum */
    --barmodel-fill-a: rgba(34, 48, 58, 0.08);
    --barmodel-fill-b: rgba(43, 93, 168, 0.18);

    /* Typography — Graph Paper preset. Newsreader serif display,
       IBM Plex Mono for meta + counters, Newsreader italic for margin
       notes. Words ON the board use KaTeX's own text face so prose and
       equations read as one hand (founder, 2026-08-12 — Public Sans words
       next to serif math looked like two different documents). */
    --font-display: "Newsreader", Georgia, serif;
    --font-body:    "Newsreader", Georgia, serif;
    --font-ui:      "Public Sans", -apple-system, BlinkMacSystemFont, sans-serif;
    --font-board:   "KaTeX_Main", "Newsreader", Georgia, serif;
    --font-mono:    "IBM Plex Mono", "SF Mono", Menlo, monospace;
    --font-hand:    "Newsreader", Georgia, serif;
}

/* --- Body / board ---------------------------------------- */
/* The shell topbar above is the same paper — the page reads as one
   continuous ruled sheet from the very top. */
.player-body {
    background-color: var(--board-surface);
    background-image: var(--paper-grid);
    background-size: 30px 30px;
    /* The window into the lesson is FIXED (founder, 2026-08-13): the page
       never grows or scrolls — title, Piero, and the speech bubbles hold
       their places, and tall board content scrolls INSIDE .board-stage
       (which stays anchored to the newest writing). The min-height chain
       this replaces let the page grow instead, pushing the chrome off
       screen on the roll. */
    height: 100vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    color: var(--board-ink);
    transition: background-color 220ms ease;
}
/* Snapshot mode (the render harness) needs the old grow-with-content page —
   it sizes the browser window to the full board height and screenshots the
   whole sheet. Restore that behavior whenever the harness's class is on. */
.player-body:has(.blackboard.snapshot-mode) {
    height: auto;
    overflow: visible;
    display: block;
}

/* --- Shell chrome in the fullscreen player ----------------
   The non-embed header reuses the base template's brand block, presented
   wordmark-only like the Tutor room: the standing mini-rig stage is hidden
   (mirrors `body.tutor-page .nb-brand-stage`), and with no nav tabs, Exit is
   pushed to the right edge like the shell's actions. The bar is ~80px tall,
   which the board's height calc below subtracts. */
.player-body .nb-brand-stage { visibility: hidden; }
.player-body .nb-topbar-actions { margin-left: auto; }

.blackboard {
    position: relative;
    background: transparent;
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 0;
}
/* Snapshot mode takes itself out of the flex column; its height rules are set
   once, further down, where they are explained. This block used to repeat them
   with min-height: calc(100vh - 80px), which the later rule then overrode —
   two declarations of the same thing, disagreeing, with the reader left to
   work out which one wins. */
.blackboard.snapshot-mode { flex: none; }
/* The notes drawer OVERLAPS the board — the board never moves (founder,
   2026-08-09). The board is a fixed-width canvas, so any attempt to squeeze or
   scale it either clipped the content or lurched on open; opening a panel now
   changes nothing about the board underneath it. */
.board-main {
    display: flex;
    flex: 1;
    min-height: 0;
    flex-direction: column;
}
.blackboard.snapshot-mode .board-main { min-height: inherit; }
/* Embedded in the Tutor session shell (iframe) — no topbar above it, so the
   board fills the frame (the body's fixed-height flex column handles it). */
.embedded .blackboard { min-height: 0; }

/* Snapshot mode ONLY (the validation harness, ?snapshot=N) — let the board grow
   to its natural height so the full-page screenshot captures everything. Normally
   .board-stage is overflow-y:auto at viewport height, so a tall board scrolls
   internally and the bottom is clipped in a fixed-viewport screenshot. The
   snapshot render path (renderSnapshot) adds this marker class; it never runs in
   normal playback, so live scrolling is untouched (founder, 2026-06-27). */
.blackboard.snapshot-mode { min-height: 0; height: auto; overflow: visible; }
/* Hide Piero + the dialogue footer in snapshots: they are persistent UI chrome,
   not authored board content. With the board collapsed to content height (above),
   the absolutely-positioned avatar (bottom:14px) rides up onto a short board and
   invents a false overlap that never happens in the full-height live player. The
   judge should score only what the author controls. (founder, 2026-06-28) */
.blackboard.snapshot-mode .board-zeno,
.blackboard.snapshot-mode .board-footer { display: none; }
.blackboard.snapshot-mode .board-stage {
    min-height: 0;
    height: auto;
    flex: none;
    overflow: visible;
}

/* --- Board header ---------------------------------------- */
.board-header {
    display: flex;
    align-items: center;
    gap: 24px;
    padding: 20px 40px 16px;
    background: transparent;
    border-bottom: 1px solid rgba(34, 48, 58, 0.12);
}
.back-link {
    color: var(--board-ink-3);
    text-decoration: none;
    font-family: var(--font-mono);
    font-size: 17px;
    line-height: 1;
    padding: 6px 10px;
    border-radius: 2px;
    transition: color 120ms ease, background 120ms ease;
}
.back-link:hover {
    color: var(--chalk);
    background: rgba(34, 48, 58, 0.06);
}
.board-lesson-title {
    /* Not flex:1 — the title takes only the room it needs so the beat counter
       sits right beside it instead of being pushed into the far corner. */
    flex: 0 1 auto;
    color: var(--chalk);
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 19px;
    letter-spacing: 0;
    margin: 0;
}
.board-progress {
    margin-right: auto;   /* the leftover space goes to its right, not its left */
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Embedded (Tutor session iframe): the board must read as ONE seamless surface,
   so the header loses its hairline (the back arrow is already omitted server-side
   in embed mode). The header row stays as an invisible structural row — the beat
   counter keeps its right-aligned slot. The lesson title starts hidden here and
   fades into the top-left slot as the intro flash fades out (fadeOutPendingTitle
   in lesson_player.js), so it "moves" from center-board to the header and PERSISTS
   there for the whole lesson. Standalone /lessons keeps today's header + arrow. */
.embedded .board-header { border-bottom: none; }
/* A slow ease + a few px of settle — the title drifts into place, never pops. */
.embedded .board-lesson-title {
    opacity: 0;
    transition: opacity 0.8s ease;
}
.embedded .board-lesson-title.header-title-shown { opacity: 1; transform: none; }

/* --- Stage ------------------------------------------------ */
.board-stage {
    position: relative;
    flex: 1;
    min-height: 0;   /* lets the stage shrink below its content — THE window */
    padding: 48px 40px 32px;
    overflow-x: hidden;
    overflow-y: auto;
    max-width: 1180px;
    width: 100%;
    margin: 0 auto;
    /* No glow on paper — the ruled sheet from .player-body shows straight
       through the stage. */
    background: transparent;
}
/* The authored composition stays capped at one desktop width in embed mode too.
   Wider screens reveal more surrounding/history instead of changing line wraps;
   narrow screens still reflow at full readable type size. */
.embedded .board-stage { max-width: 1180px; }

.blackboard-inner {
    position: relative;
    color: var(--board-ink);
    font-family: var(--font-board);
    font-size: 1.15rem;
    line-height: 1.55;
    max-width: 100%;
    transition: max-width 0.55s cubic-bezier(0.4, 0, 0.2, 1),
                transform 0.55s cubic-bezier(0.4, 0, 0.2, 1),
                opacity   0.55s ease-out,
    filter    0.55s ease-out;
}
#blackboard-inner { min-height: 100%; }

/* During an interrupt the spine yields its right side to the scratch pane.
   It does NOT scale down — a uniform scale made the content tiny (and with
   two columns, doubly so: each column was already half-width, then scaled
   again). Instead the spine REFLOWS into the left region by narrowing its
   max-width: text keeps its size, and a surface shrinks only as much as its
   own narrower column needs. The block is left-aligned, so narrowing the
   width simply leaves it sitting in the left region; max-width animates, so
   the hand-off is a smooth slide, not a pop. The scratch pane covers the
   rest (50%, or a third when there are two columns). */
.blackboard.interrupt-mode .blackboard-inner {
    max-width: calc(50% - 16px);
    opacity: 0.9;
    filter: saturate(0.95);
}

/* Two lesson columns: the spine keeps the left two-thirds (1/3 + 1/3) and
   the scratch pane takes the remaining third. The columns reflow to a third
   each at full size — no extra shrink. (founder contract 2026-06-12) */
.blackboard.interrupt-mode[data-cols="2"] .blackboard-inner {
    max-width: calc(66% - 16px);
}
.blackboard[data-cols="2"] .scratch-region {
    width: 33.5%;
}

/* Native authors own their visual state. These automatic legacy treatments
   apply only to the older board renderer; shell layout remains shared. */
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .reveal-char,
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .reveal-token,
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .reveal-pop,
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .reveal-row,
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .rect-box {
    transition: none !important;
    opacity: 1 !important;
    transform: none !important;
    transition-delay: 0s !important;
}

/* --- Column layout (founder contract 2026-06-12, panel-break model 06-13) ---
   The lesson flows into up to two side-by-side panels. Panel 1 spans the
   full board; a `panel_break` directive (author intent) flips data-cols to
   "2", and the flex-basis transition slides panel 1 left while panel 2 fades
   in beside it — the elegant shift, no instant pop. An authored `clear`
   resets to one panel. Max two panels: content that overflows a panel just
   scrolls (the founder's rule — scrolling is fine, breakage never). */
:where(body:not(.native-direct-board-player)) .board-cols {
    display: flex;
    align-items: flex-start;
    gap: 28px;
}
:where(body:not(.native-direct-board-player)) .board-col {
    flex: 0 0 100%;
    min-width: 320px;   /* readability floor — the spine is never squished too small */
    transition: flex-basis 0.65s cubic-bezier(0.4, 0, 0.2, 1),
                opacity    0.45s ease-out 0.25s,
                transform  0.45s ease-out 0.25s;
}
/* One panel: full width, so the floor never forces overflow. Widths key off
   the ROW's own data-cols (each .board-cols row stamps its count), so in roll
   mode an older two-column section keeps its split after the live section
   resets to one. The blackboard-level dataset still mirrors the active
   section for the interrupt split rules below. */
:where(body:not(.native-direct-board-player)) .board-cols[data-cols="1"] > .board-col { min-width: 0; }
:where(body:not(.native-direct-board-player)) .board-cols[data-cols="2"] > .board-col {
    flex-basis: calc(50% - 14px);
}
/* Legacy fallback (a rebuild-restored row without its own dataset). */
:where(body:not(.native-direct-board-player)) .blackboard[data-cols="1"] .board-cols:not([data-cols]) > .board-col { min-width: 0; }
:where(body:not(.native-direct-board-player)) .blackboard[data-cols="2"] .board-cols:not([data-cols]) > .board-col {
    flex-basis: calc(50% - 14px);
}

/* Page-layout rewind dips out while it rebuilds. Keep every base transition
   property alive: a second `.blackboard-inner { transition: opacity }` rule
   used to silently delete the max-width/transform/filter transitions and made
   interrupt reflow jump. Roll-layout rewind travels along the sheet instead. */
.blackboard-inner.board-rewinding {
    opacity: 0;
    transition: max-width 0.34s cubic-bezier(0.4, 0, 0.2, 1),
                transform 0.34s cubic-bezier(0.4, 0, 0.2, 1),
                opacity   0.34s ease,
                filter    0.34s ease;
}

/* Roll mode (founder, 2026-08-12): the board is a vertical roll of sections.
   The section seam is structural, NOT drawn (founder, 2026-08-13): the student
   sees only a breathing gap of paper between blocks of work — no line. The
   closed-marker class still gates the engine (Notes capture, break logic). */
:where(body:not(.native-direct-board-player)) .board-section { padding-bottom: 6px; }
:where(body:not(.native-direct-board-player)) .board-section-closed {
    padding-bottom: 22px;
    margin-bottom: 26px;
}
/* A newborn column 2 waits just off to the right while column 1 slides,
   then settles in (the opacity/transform transition above). */
:where(body:not(.native-direct-board-player)) .board-col.board-col-new {
    opacity: 0;
    transform: translateX(28px);
}
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .board-col {
    transition: none !important;
}
/* Surfaces shrink to fit a column rather than overflow it (SVGs carry a
   viewBox, so they scale cleanly and all geometry math still holds). */
:where(body:not(.native-direct-board-player)) .board-col svg.board-plane,
:where(body:not(.native-direct-board-player)) .board-col svg.board-numline,
:where(body:not(.native-direct-board-player)) .board-col svg.board-barmodel,
:where(body:not(.native-direct-board-player)) .board-col svg.board-shape {
    max-width: 100%;
    height: auto;
}

/* "Ready to begin" — the one waiting-room serif moment. */
.blackboard-placeholder {
    color: var(--board-ink-3);
    font-family: var(--font-hand);
    font-style: italic;
    font-size: 17px;
    text-align: center;
    margin-top: 8rem;
}

/* Hand-raised indicator (functional state cue). */
.hand-indicator {
    position: absolute;
    top: 5.25rem;
    right: 1.5rem;
    opacity: 0;
    transition: opacity 0.2s;
    pointer-events: none;
    z-index: 20;
}
.hand-indicator svg { width: 30px; height: 30px; fill: none; stroke: var(--board-ink); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.hand-indicator.visible { opacity: 1; }
body[data-player-state="INTERRUPT_PENDING"] .hand-indicator {
    opacity: 1;
    animation: handWave 0.8s ease-in-out infinite;
}
@keyframes handWave {
    0%, 100% { transform: rotate(-8deg); }
    50%      { transform: rotate(8deg); }
}

/* --- Per-directive rendering ----------------------------- */

/* The lesson title flashes on at the open and holds through the intro
   narration, then fades out smoothly just before the first board action lands
   (see advance / fadeOutPendingTitle) — a gentle hand-off, never a pop, and not
   a standing element, since the lesson name also shows in the app chrome. */
.board-title {
    font-family: var(--font-display);
    font-size: 2rem;
    font-weight: 500;
    color: #22303A;
    text-align: center;
    margin: 0 0 1.25rem;
    letter-spacing: 0;
    transition: opacity 0.45s ease;
}
.board-title.board-title-out { opacity: 0; }

.board-bignum {
    font-family: var(--font-display);
    font-size: 5.5rem;
    font-weight: 500;
    color: #22303A;
    text-align: center;
    margin: 0.5rem 0 1rem;
    line-height: 1;
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
}

.board-equation {
    color: #22303A;
    margin: 0.45rem 0;
    text-align: center;
}
.board-equation .katex {
    font-size: 1.45rem;
    color: #22303A;
}
.board-equation .katex .mord,
.board-equation .katex .mbin,
.board-equation .katex .mrel {
    color: #22303A;
}

.board-write {
    font-family: var(--font-board);
    font-size: 1.2rem;
    color: var(--board-ink);
    /* Shrink-wrapped and centred rather than full-width-centred, so a step_label
       tag can hang off the END OF THE WORDS. A line long enough to wrap still
       fills the panel and wraps identically (fit-content = max-content capped by
       the available width), so nothing about existing boards changes. */
    width: fit-content;
    max-width: 100%;
    margin: 0.6rem auto;
    text-align: center;
}

.board-note {
    font-family: var(--font-hand);
    font-style: italic;
    font-size: 0.95rem;
    color: var(--board-ink-3);
    margin: 0.5rem 0;
    text-align: center;
}

/* --- answer_slot: the checkpoint typed-answer box (founder, 2026-07-14) ---
   A KaTeX label ('x =') beside an empty ink-hairline blank the student writes
   in — plus an optional suffix after the box (inline form, founder 2026-07-17),
   so 'label [box] suffix' reads as one expression. Static during playback;
   runCheckpointFlow drops an <input> into the box while the student is
   answering, then locks it. Graph Paper tokens only. */
.board-answer-slot {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    margin: 0.9rem 0;
}
.answer-slot-label,
.answer-slot-suffix {
    color: #22303A;
}
.answer-slot-label .katex,
.answer-slot-suffix .katex {
    font-size: 1.45rem;
    color: #22303A;
}
.answer-slot-box {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    /* A blank in the expression, not a form field: sized like a wide character
       slot (fits a sign or a two-digit answer empty), growing with what the
       student types. The old 140px floor dwarfed short expressions like
       -7 [box] -3 (founder, 2026-07-18). */
    min-width: 3.2rem;
    min-height: 2.2rem;
    padding: 0.15rem 0.45rem;
    border: 1.5px solid var(--chalk-box);      /* ink hairline — "write here" */
    border-radius: 4px;
    background: #FFFFFF;
}
.board-answer-slot.is-active .answer-slot-box {
    border-color: var(--accent);               /* red while the student is typing */
    box-shadow: 0 0 0 3px var(--accent-soft);
}
.answer-slot-box.is-locked {
    border-style: solid;
    background: var(--accent-soft);
}
.answer-slot-input {
    flex: 0 1 auto;
    min-width: 1.6rem;                          /* hugs a short answer; grows as typed */
    border: none;
    outline: none;
    background: transparent;
    color: #22303A;
    font-family: var(--font-mono);
    font-size: 1.45rem;                         /* matches the board equation scale */
    text-align: center;
}
/* The hidden same-font measurer makeMathField uses to auto-grow the slot. */
.answer-slot-measure {
    position: absolute;
    visibility: hidden;
    white-space: pre;
    font-family: var(--font-mono);
    font-size: 1.45rem;
}
.answer-slot-input::placeholder { color: var(--board-ink-3); }
.answer-slot-submit {
    border: none;
    background: transparent;
    color: var(--accent);
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    padding: 2px 4px;
}
.answer-slot-value {
    color: #22303A;
    font-family: var(--font-mono);
    font-size: 1.2rem;
    font-variant-numeric: tabular-nums;
}

/* MathLive's in-field buttons (virtual-keyboard toggle, menu toggle) are
   chrome the board doesn't want — the math keyboard already opens on focus
   (see makeMathField's focusin handler). Hidden everywhere in the player. */
math-field::part(virtual-keyboard-toggle) { display: none; }
math-field::part(menu-toggle) { display: none; }

/* --- Tables: ink grid on the paper ------------------------ */
.board-table {
    border-collapse: collapse;
    margin: 1.25rem auto;
    color: var(--board-ink);
    font-family: var(--font-board);
    font-size: 1rem;
    font-variant-numeric: tabular-nums;
    border: 1.5px solid var(--chalk-box);
}
.board-table th,
.board-table td {
    border: 1px solid rgba(34, 48, 58, 0.18);
    padding: 0.5rem 1.4rem;
    text-align: center;
    min-width: 3.5rem;
}
.board-table th {
    font-family: var(--font-mono);
    font-weight: 500;
    color: var(--board-ink-3);
    background: rgba(34, 48, 58, 0.04);
    text-transform: uppercase;
    font-size: 0.7rem;
    letter-spacing: 0.18em;
    border-bottom: 1.5px solid var(--chalk-box);
}
.board-table tr.struck td:first-child {
    text-decoration: line-through;
    text-decoration-thickness: 2px;
    text-decoration-color: var(--accent);
}
.board-table tr.struck td { opacity: 0.5; }

/* --- Rectangles --- */
.board-rectangles {
    display: flex;
    gap: 1.75rem;
    justify-content: center;
    align-items: flex-end;
    margin: 1.25rem 0;
    flex-wrap: wrap;
    --cell: 14px;
}
.rect-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.55rem;
}
/* The 1px gaps between cells show the parent background — that IS the grid
   line, so the parent carries the ink tone and each cell re-fills with the
   board surface. */
.rect-grid {
    display: grid;
    gap: 1px;
    background: rgba(34, 48, 58, 0.45);
    padding: 1.5px;
    border-radius: 0;
}
.rect-cell {
    background: var(--board-surface);
    width: var(--cell);
    height: var(--cell);
}
.rect-dims {
    font-family: var(--font-mono);
    font-size: 0.85rem;
    letter-spacing: 0.04em;
    color: var(--board-ink);
    font-variant-numeric: tabular-nums;
}
.rect-label {
    font-family: "Newsreader", Georgia, serif;
    font-style: italic;
    font-size: 0.85rem;
    color: var(--board-ink-3);
}

/* --- Annotations --- */
/* Boxed step: the ink-rule box. */
.with-box,
.annot-box {
    position: relative;
    padding: 0.4rem 0.75rem;
    border: 1.5px solid var(--chalk-box);
    border-radius: 2px;
    background: transparent;
    display: block;
    width: fit-content;
    margin: 0.5rem auto;
    box-sizing: border-box;
}
/* The tutor's "look here" box: red rule + red wash. */
.annot-box {
    border-color: var(--accent);
    background: rgba(194, 64, 42, 0.07);
    animation: drawBoxIn 0.4s ease-out;
}
@keyframes drawBoxIn {
    from { opacity: 0; transform: scale(0.95); }
    to   { opacity: 1; transform: scale(1); }
}
/* AI "highlight this" — the red wash over the board. */
.highlighted {
    background: rgba(194, 64, 42, 0.28);
    border: none;
    border-radius: 2px;
    padding: 0.12em 0.28em;
}

/* --- Strikethrough --- */
.board-write.struck,
.board-equation.struck {
    text-decoration: line-through;
    text-decoration-thickness: 2px;
    text-decoration-color: var(--accent);
    opacity: 0.5;
}
/* text-decoration never crosses KaTeX's inline-block boxes, so on math the
   line-through above is invisible. Draw the strike as an overlaid rule on the
   .katex element instead — it shrink-wraps the math exactly (the eq_cancel
   diagonal already uses this technique). */
.board-write.struck .katex,
.board-equation.struck .katex {
    position: relative;
}
.board-write.struck .katex::after,
.board-equation.struck .katex::after {
    content: '';
    position: absolute;
    left: -2%;
    right: -2%;
    top: 50%;
    height: 2px;
    background: var(--accent);
}

/* --- v2 equation-manipulation ops ---
   eq_move / eq_sub / eq_highlight / eq_combine / eq_div / eq_check */
/* The flown clone (fixed-position, removed on landing). */
.eq-fly {
    pointer-events: none;
    will-change: transform;
}
/* The term's old spot after it travels — "this left here". */
.eq-mark-src.eq-moved {
    opacity: 0.4;
    transition: opacity 0.45s ease;
}
/* Semantic substitution: first name the expression being reused and the
   variable slot it replaces, then let the token choreography do the work. */
.eq-transfer-source,
.eq-transfer-target {
    border-radius: 4px;
    padding: 0 0.1em;
    transition: background 0.24s ease, box-shadow 0.24s ease, opacity 0.4s ease;
}
.eq-transfer-source.is-active {
    background: rgba(194, 64, 42, 0.16);
    box-shadow: 0 2px 0 rgba(194, 64, 42, 0.72);
}
.eq-transfer-target.is-active {
    background: rgba(194, 64, 42, 0.24);
    box-shadow: 0 0 0 2px rgba(194, 64, 42, 0.38);
}
.eq-transfer-target.eq-moved {
    opacity: 0.3;
    box-shadow: none;
}
/* Persistent term tint (eq_highlight). The padding grows the chip OUTWARD over
   KaTeX's own spacing, so a term sitting next to a relation sign ('5' in
   '50 × 5 = 250') had its chip land flush against the '=' — the gap the reader
   uses to parse the equation was eaten by the tint. The margin buys that gap
   back in layout (it pushes the neighbour, unlike padding), so a highlighted
   term always breathes on both sides. */
.eq-hl {
    background: rgba(194, 64, 42, 0.35);
    border-radius: 4px;
    padding: 0 0.12em;
    margin: 0 0.12em;
}
/* Just-substituted expression — tint that fades out. */
.eq-sub-new {
    border-radius: 4px;
    padding: 0 0.12em;
    animation: eq-sub-fade 2.4s ease forwards;
}
@keyframes eq-sub-fade {
    from { background: rgba(194, 64, 42, 0.5); }
    to   { background: rgba(194, 64, 42, 0); }
}
/* eq_div balance gesture — "÷ N" written under each side, then gone.
   Positioned (left/top) inline by the player; centered + animated here. */
.eq-div-badge {
    font-size: 0.95rem;
    font-style: italic;
    color: var(--accent);
    opacity: 0;
    transform: translateX(-50%) translateY(-3px);
    transition: opacity 0.3s ease-out, transform 0.3s ease-out;
}
.eq-div-badge.shown {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}
.eq-div-badge.eq-div-badge-out {
    opacity: 0;
    transition: opacity 0.4s ease;
}
/* eq_div fraction style — denominators and the equals fade in after the
   numerators land and the bars draw. */
.eq-frac-pending {
    opacity: 0;
    transition: opacity 0.25s ease-out;
}
.eq-frac-pending.shown {
    opacity: 1;
}
/* eq_cancel — the diagonal cancel slash on a matched pair. */
.eq-cancel-mark {
    position: relative;
    display: inline-block;
}
.eq-cancel-mark::after {
    content: '';
    position: absolute;
    left: -12%;
    right: -12%;
    top: 50%;
    height: 2px;
    background: var(--accent);
    transform: rotate(-22deg) scaleX(0);
    transform-origin: left center;
    transition: transform 0.28s ease-out;
}
.eq-cancel-mark.struck-diag {
    opacity: 0.55;
    transition: opacity 0.3s ease 0.2s;
}
.eq-cancel-mark.struck-diag::after {
    transform: rotate(-22deg) scaleX(1);
}
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .eq-cancel-mark.struck-diag,
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .eq-cancel-mark.struck-diag::after {
    transition: none !important;
}

/* eq_check — a tick drawn beside a verified line. Red, like the canvas's
   rotated "check this ✓" margin note — the one annotation accent. */
.eq-tick {
    width: 0.95em;
    height: 0.95em;
    margin-left: 0.45em;
    vertical-align: -0.12em;
    overflow: visible;
}
.eq-tick path {
    fill: none;
    stroke: var(--accent);
    stroke-width: 3.2;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-dasharray: 30;
    stroke-dashoffset: 30;
    animation: eq-tick-draw 0.5s ease-out 0.15s forwards;
}
@keyframes eq-tick-draw {
    to { stroke-dashoffset: 0; }
}
/* Rebuild replay: the tick is state — render it already drawn. */
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .eq-tick path,
:where(body:not(.native-direct-board-player)) .no-reveal .eq-tick path {
    animation: none;
    stroke-dashoffset: 0;
}

/* step_label — a compact tag in the margin naming the recipe step a worked
   line executes. Pinned just past the right edge of the rendered EQUATION (the
   inline `.math-container`), not the full-width centered line — so on a short
   equation in a wide panel it stays tight to the math instead of floating off
   at the panel edge, and it never overprints a boxed/narrow line. The centered
   equation never shifts (the tag is absolutely positioned outside the content
   box). Faint, smaller, uppercase so it reads as a label, not as math. Board
   state — persists and re-renders on rebuild. */
.math-container.has-step-label {
    position: relative;
    display: inline-block;
}
/* A tagged PROSE WRITE line (2026-08-02): a write has no inline math container,
   so the tag anchors to the line box itself. `.board-write` shrink-wraps its
   text (width: fit-content, auto margins), so `left: 100%` lands just past the
   words instead of out at the panel edge — the same tight seat the equation
   tag gets, and the centring is unchanged. */
.board-write.has-step-label {
    position: relative;
}
.eq-step-label {
    position: absolute;
    left: 100%;
    margin-left: 0.85rem;
    top: 50%;
    transform: translateY(-50%);
    font-family: var(--font-mono);
    font-size: 0.7rem;
    font-weight: 500;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: #22303A;
    opacity: 0.5;
    white-space: nowrap;
    pointer-events: none;
    animation: eq-step-label-in 0.4s ease-out both;
}
@keyframes eq-step-label-in {
    from { opacity: 0; transform: translateY(-50%) translateX(0.3rem); }
    to   { opacity: 0.5; transform: translateY(-50%) translateX(0); }
}
/* Tick + tag on the SAME line — the recipe's final answer is both the verified
   line and the step that completes the routine. Both anchor to the math's right
   edge, so by default the tag (left:100%) overprints the inline tick into a
   garbled glyph ("✓TEP 4"). When the player marks the container .has-eq-tick
   (only when both are present), push the tag clear of the tick: the tick box is
   ~0.95em wide + 0.45em margin in the equation's 1.45rem em (~2rem total), so a
   2.6rem offset seats the tag just past it with a clean gap. Tick reads first,
   tag second — the natural left-to-right order. */
.math-container.has-step-label.has-eq-tick .eq-step-label {
    margin-left: 2.6rem;
}
/* Rebuild replay: the label is state — render it already settled. */
:where(body:not(.native-direct-board-player)) .blackboard-inner.no-reveal .eq-step-label,
:where(body:not(.native-direct-board-player)) .no-reveal .eq-step-label {
    animation: none;
}

/* --- Reveal animations --- */
:where(body:not(.native-direct-board-player)) .reveal-char {
    display: inline-block;
    opacity: 0;
    transform: translateY(2px);
    transition: opacity 0.18s ease-out, transform 0.18s ease-out;
}
:where(body:not(.native-direct-board-player)) .reveal-char.shown { opacity: 1; transform: translateY(0); }

:where(body:not(.native-direct-board-player)) .reveal-token {
    opacity: 0;
    transform: translateY(2px);
    transition: opacity 0.22s ease-out, transform 0.22s ease-out;
}
:where(body:not(.native-direct-board-player)) .reveal-token.shown { opacity: 1; transform: translateY(0); }

:where(body:not(.native-direct-board-player)) .reveal-pop {
    opacity: 0;
    transform: scale(0.92);
    transition: opacity 0.45s ease-out, transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
:where(body:not(.native-direct-board-player)) .reveal-pop.shown { opacity: 1; transform: scale(1); }

:where(body:not(.native-direct-board-player)) .reveal-row {
    opacity: 0;
    transform: translateX(-12px);
    transition: opacity 0.35s ease-out, transform 0.4s ease-out;
}
:where(body:not(.native-direct-board-player)) .reveal-row.shown { opacity: 1; transform: translateX(0); }
:where(body:not(.native-direct-board-player)) .reveal-row td,
:where(body:not(.native-direct-board-player)) .reveal-row td * { opacity: 1 !important; transform: none !important; }

/* --- AI manipulation effects --- */
:where(body:not(.native-direct-board-player)) [id^="obj_"] {
    transition: opacity 0.4s ease, transform 0.4s ease, filter 0.4s ease;
}
:where(body:not(.native-direct-board-player)) .state-dimmed { opacity: 0.3; filter: saturate(0.6); }
/* Pointing must be unmistakable: recolor to red (the same treatment plane
   objects get below), not just a glow — a faint shadow on ink text is
   invisible. The KaTeX descendants and table headers set
   their own explicit colors, so the recolor must name them too (inheritance
   alone loses to those rules). */
:where(body:not(.native-direct-board-player)) .state-highlighted {
    color: var(--accent);
    transform: scale(1.05);
    filter: drop-shadow(0 0 10px rgba(194, 64, 42, 0.35));
}
:where(body:not(.native-direct-board-player)) .state-highlighted .katex,
:where(body:not(.native-direct-board-player)) .state-highlighted .katex .mord,
:where(body:not(.native-direct-board-player)) .state-highlighted .katex .mbin,
:where(body:not(.native-direct-board-player)) .state-highlighted .katex .mrel,
:where(body:not(.native-direct-board-player)) .state-highlighted th {
    color: var(--accent);
}
:where(body:not(.native-direct-board-player)) .state-hidden { opacity: 0; transform: scale(0.95); pointer-events: none; }

/* The scale() pop above is for DOM equation lines. On an SVG surface it would
   pivot around the SVG's (0,0) origin and visibly SHIFT the object — a plotted
   dot slides sideways — instead of growing in place. Surfaces highlight by
   recoloring (the rules further down), so drop the transform there: a straight
   highlight, no movement. */
:where(body:not(.native-direct-board-player)) svg.board-plane .state-highlighted, :where(body:not(.native-direct-board-player)) svg.board-plane .state-hidden,
:where(body:not(.native-direct-board-player)) svg.board-numline .state-highlighted, :where(body:not(.native-direct-board-player)) svg.board-numline .state-hidden,
:where(body:not(.native-direct-board-player)) svg.board-barmodel .state-highlighted, :where(body:not(.native-direct-board-player)) svg.board-barmodel .state-hidden,
:where(body:not(.native-direct-board-player)) svg.board-shape .state-highlighted, :where(body:not(.native-direct-board-player)) svg.board-shape .state-hidden {
    transform: none;
}

.annot-underline { position: relative; }
.annot-underline::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -4px;
    height: 3px;
    background: var(--accent);
    border-radius: 0;
    transform-origin: left;
    animation: drawLineIn 0.4s ease-out forwards;
}
@keyframes drawLineIn {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
}

/* --- Scratch region — seamless same surface --- */
.scratch-region {
    position: absolute;
    top: 0;
    right: 0;
    width: 50%;
    height: 100%;
    padding: 32px 40px;
    box-sizing: border-box;
    /* Opaque paper, ruled with the SAME grid as the page. `fixed` anchors the
       grid to the viewport rather than to this panel's own box, so the ruling
       lines up across the seam and spine + scratch read as one sheet. */
    background-color: var(--board-surface);
    background-image: var(--paper-grid);
    background-size: 30px 30px;
    background-attachment: fixed;
    /* Divider hidden — spine and scratch read as one continuous surface. */
    border-left: none;
    overflow-y: auto;
    transform: translateX(110%);
    opacity: 0;
    pointer-events: none;
    transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1),
                opacity   0.55s ease-out,
                background-color 220ms ease;
    display: flex;
    flex-direction: column;
    justify-content: center;
}
.blackboard.interrupt-mode .scratch-region {
    transform: translateX(0);
    opacity: 1;
    pointer-events: auto;
}
/* The AI's panel matches the spine's standards. Base font + line-height
   equal the spine board (.blackboard-inner), and the board-* elements carry
   no scratch-specific overrides — the shared .board-equation/.board-write/
   .board-bignum/.board-table rules apply, so the tutor's hand looks identical
   whether the lesson wrote it or the live AI did (interrupts, replays). */
.scratch-inner {
    color: var(--board-ink);
    font-family: var(--font-board);
    font-size: 1.15rem;
    line-height: 1.55;
}
/* The one scratch-specific rule: surfaces fit the narrow pane. The viewBox
   keeps their geometry true at any rendered size. */
.scratch-inner svg.board-plane,
.scratch-inner svg.board-numline,
.scratch-inner svg.board-barmodel,
.scratch-inner svg.board-shape {
    max-width: 100%;
    height: auto;
}

/* --- Footer ---------------------------------------------- */
.board-footer {
    padding: 18px 40px 28px;
    border-top: none;
    background: transparent;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* Speech bubbles — the Graph Paper pair: the student's line is a WHITE card
   (what the student hands up); the tutor's is a faint ballpoint-washed
   outline on the page (speech written on the sheet). Tail = one tight 4px corner toward the speaker; the
   old triangle tails are gone. */
.speech-bubble {
    position: relative;
    background: rgba(43, 93, 168, 0.04);
    border: 1px solid rgba(34, 48, 58, 0.22);
    border-radius: 16px;
    padding: 14px 20px;
    color: rgba(34, 48, 58, 0.9);
    font-family: var(--font-body);
    font-size: 1.05rem;
    line-height: 1.55;
    min-height: 2.2rem;
    transition: background 0.2s, border-color 0.2s;
}

/* No triangle tails — the asymmetric corner is the tail. */
.speech-bubble::before,
.speech-bubble::after {
    content: none;
}

/* Piero's ink rig — bottom-left corner of the board; the tutor bubble shifts
   right to clear him, its tail corner pointing back at him (founder, 2026-06-22). */
.player-body { --board-zeno-height: 106px; --board-zeno-bottom: 14px; }
.player-body.embedded { --board-zeno-bottom: 20px; }
.board-zeno { position: absolute; left: 20px; bottom: var(--board-zeno-bottom); width: 88px; height: var(--board-zeno-height); z-index: 6; pointer-events: none; }
.embedded .board-zeno { left: 28px; }

/* Tutor bubble: faint outline on the page, tail corner toward Piero at bottom-left. */
.tutor-bubble {
    align-self: flex-start;
    max-width: 72%;
    margin-left: 108px; /* clear Piero standing there */
    border-radius: 16px 16px 16px 4px;
    padding: 13px 19px;
}
.tutor-bubble .speech-text {
    font-family: var(--font-body);
    font-size: 17px;
    color: rgba(34, 48, 58, 0.9);
}

/* Student bubble: a white card, tail corner bottom-right (mirror). */
.student-bubble {
    align-self: flex-end;
    max-width: 78%;
    margin-right: 20px;
    background: #FFFFFF;
    border: 1px solid rgba(34, 48, 58, 0.14);
    /* Same rounded shape as the tutor bubble, tail on the student's side
       (founder, 2026-07-12 — replaces the handoff's letter-card corners). */
    border-radius: 16px 16px 4px 16px;
    box-shadow: 0 4px 14px rgba(34, 48, 58, 0.05);
    color: #22303A;
    padding: 14px 18px;
}
.student-bubble .speech-text {
    font-family: var(--font-body);
    font-size: 15.5px;
    color: #22303A;
}
.student-bubble:has(.speech-text[contenteditable="true"]) {
    padding-bottom: 22px;
}
.student-bubble .speech-text[contenteditable="true"] {
    min-width: 12rem;
    min-height: 1.55em;
    outline: none;
    white-space: pre-wrap;
}
.typed-enter {
    position: absolute;
    right: 8px;
    bottom: 5px;
    padding: 1px 5px;
    border: 1px solid rgba(34, 48, 58, 0.25);
    border-radius: 3px;
    background: #FFFFFF;
    color: rgba(34, 48, 58, 0.65);
    font-family: var(--font-mono);
    font-size: 9px;
    line-height: 1.35;
    text-transform: lowercase;
    cursor: pointer;
    transition: color 120ms ease, border-color 120ms ease;
}
.typed-enter:hover {
    color: var(--accent);
    border-color: var(--accent);
}

.speech-label {
    font-family: var(--font-mono);
    font-size: 0.62rem;
    font-weight: 500;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--board-ink-3);
    margin-bottom: 4px;
}
.student-bubble .speech-label {
    font-family: var(--font-ui);
    font-size: 11px;
    font-weight: 400;
    letter-spacing: 0.01em;
    text-transform: none;
    color: #8B97A1;
    margin-bottom: 5px;
}
.speech-text {
    word-wrap: break-word;
}

/* Status / instruction strip */
.status-bar {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--board-ink-3);
    text-align: center;
    padding: 6px 0 2px;
    min-height: 1.5rem;
}
.status-bar:empty { display: none; }
.status-bar kbd {
    background: rgba(34, 48, 58, 0.06);
    border: 1px solid rgba(34, 48, 58, 0.25);
    border-radius: 2px;
    padding: 1px 6px;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    font-weight: 500;
    color: var(--board-ink);
    letter-spacing: 0.05em;
}
/* Recording = the red pen. */
.status-bar.recording {
    color: var(--accent);
    font-weight: 600;
}
.status-bar.recording::before {
    content: "● ";
    color: var(--accent);
    animation: pulseDot 1s infinite;
}
@keyframes pulseDot {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.3; }
}

/* --- Controls --- */
.board-controls {
    display: flex;
    flex-direction: column;
    gap: 12px;
    align-items: center;
    justify-content: center;
}

.lesson-controls {
    display: flex;
    align-items: stretch;
    border: 1px solid rgba(34, 48, 58, 0.22);
    background: rgba(250, 248, 242, 0.88);
}
.lesson-controls[hidden] { display: none; }
/* The player's turn state owns availability. The button's flex styling must
   never expose a control whose hidden attribute is set. */
.lesson-control[hidden] { display: none !important; }
.lesson-control {
    min-width: 116px;
    min-height: 48px;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 9px;
    padding: 7px 12px;
    border: 0;
    border-right: 1px solid rgba(34, 48, 58, 0.18);
    border-radius: 0;
    background: transparent;
    color: var(--board-ink);
    cursor: pointer;
    touch-action: none;
    transition: color 120ms ease, background 120ms ease;
}
.lesson-control:last-child { border-right: 0; }
.lesson-control:hover:not(:disabled) { background: rgba(34, 48, 58, 0.045); }
.lesson-control:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.lesson-control:disabled { color: rgba(34, 48, 58, 0.3); cursor: default; }
.lesson-control svg {
    width: 21px;
    height: 21px;
    flex: 0 0 auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.lesson-control .response-mic-icon { display: none; fill: currentColor; stroke: none; }
.lesson-control.is-response .raise-hand-icon { display: none; }
.lesson-control.is-response .response-mic-icon { display: block; }
.lesson-control-copy { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.lesson-control-copy > span {
    font-family: var(--font-mono);
    font-size: 9.5px;
    font-weight: 500;
    letter-spacing: 0.11em;
    text-transform: uppercase;
    white-space: nowrap;
}
.lesson-control kbd {
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--board-ink-3);
    font-family: var(--font-mono);
    font-size: 8px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.lesson-control.is-active,
.lesson-control.is-recording { color: var(--accent); background: var(--accent-soft); }
.lesson-control.is-recording svg { animation: pulseDot 1s infinite; }
.lesson-control .resume-icon { display: none; }
.lesson-control.is-resume .break-icon { display: none; }
.lesson-control.is-resume .resume-icon { display: block; }
.pencil-control[hidden] { display: none; }
.listening-tool-control[hidden] { display: none; }

/* The student's highlights float on their own layer above the board. Nothing
   here can move the lesson's own content: the layer is out of flow and the
   marks never take pointer events (clearing one is a hit test in JS). */
.student-mark-layer {
    position: absolute;
    inset: 0;
    z-index: 8;
    pointer-events: none;
}
.student-mark {
    position: absolute;
    border-radius: 2px;
    background: rgba(194, 64, 42, 0.18);
    box-shadow: 0 0 0 1px rgba(194, 64, 42, 0.28);
    pointer-events: none;
}
.student-work-area {
    position: sticky;
    z-index: 12;
    bottom: 0;
    width: 100%;
    margin-top: auto;
    padding: 0.65rem 0.9rem 0.8rem;
    border-top: 1px solid rgba(34, 48, 58, 0.22);
    background: rgba(250, 248, 242, 0.97);
}
.student-work-area:not(.is-open):not(.has-committed) { display: none; }
.student-work-area:not(.is-open) {
    position: relative;
    padding-block: 0.45rem;
}
.student-work-area:not(.is-open) .student-work-label,
.student-work-area:not(.is-open) .student-work-editor { display: none; }
.student-work-label {
    margin-bottom: 0.45rem;
    color: rgba(34, 48, 58, 0.62);
    font-family: var(--font-mono);
    font-size: 0.66rem;
    letter-spacing: 0.13em;
}
.student-work-editor { display: flex; align-items: center; gap: 0.5rem; }
.student-board-writing {
    display: block;
    flex: 1;
    min-width: 0;
    padding: 0.35rem 0.45rem;
    border: 1px solid rgba(43, 93, 168, 0.3);
    background: transparent;
    color: #2B5DA8;
    font-size: 1.12rem;
}
.student-work-enter {
    padding: 0.34rem 0.55rem;
    border: 1px solid rgba(34, 48, 58, 0.22);
    border-radius: 3px;
    background: transparent;
    color: #22303A;
    font-family: var(--font-mono);
    font-size: 0.65rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
}
.student-work-line { color: #2B5DA8; font-family: var(--font-ui); font-size: 1.08rem; line-height: 1.45; }
.student-work-area .ML__keyboard {
    position: relative;
    inset: auto;
    width: 100%;
    margin: 0.65rem 0 -0.8rem;
}
.markup-selection-armed { user-select: text; -webkit-user-select: text; }
.blackboard.highlight-armed .board-stage { cursor: crosshair; }
.blackboard.write-armed .board-stage { cursor: text; }

.pages-drawer {
    position: absolute;
    inset: 0 0 0 auto;
    z-index: 20;
    width: var(--notes-width, 400px);
    overflow-y: auto;
    background: #FAF8F2;
    border-left: 1px solid rgba(34, 48, 58, 0.22);
    box-shadow: -14px 0 30px rgba(34, 48, 58, 0.12);
    transform: translateX(102%);
    visibility: hidden;
    transition: transform 220ms ease, visibility 0s linear 220ms;
}
.pages-resize-handle {
    position: absolute;
    inset: 0 auto 0 -6px;
    z-index: 2;
    width: 12px;
    cursor: col-resize;
    touch-action: none;
}
.pages-resize-handle::after {
    content: "";
    position: absolute;
    inset: 0 auto 0 5px;
    width: 1px;
    background: rgba(34, 48, 58, 0.22);
}
.pages-resize-handle:hover::after,
.pages-resize-handle.is-dragging::after,
.pages-resize-handle:focus-visible::after { width: 2px; background: var(--accent); }
.pages-resize-handle:focus-visible { outline: none; }
.pages-drawer.is-open {
    transform: translateX(0);
    visibility: visible;
    transition-delay: 0s;
}
.pages-drawer-header {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 18px 20px;
    background: #FAF8F2;
    border-bottom: 1px solid rgba(34, 48, 58, 0.18);
    color: var(--board-ink-2);
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
}
.pages-tabs { display: flex; align-items: center; gap: 18px; }
.pages-tab {
    padding: 6px 0;
    border: 0;
    border-bottom: 2px solid transparent;
    background: transparent;
    color: var(--board-ink-3);
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    cursor: pointer;
}
.pages-tab.is-active { border-bottom-color: var(--accent); color: var(--accent); }
.pages-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.pages-close {
    display: grid;
    width: 30px;
    height: 30px;
    padding: 0;
    place-items: center;
    border: 0;
    background: transparent;
    color: var(--board-ink-2);
    cursor: pointer;
}
.pages-close:hover { color: var(--accent); }
.pages-close:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.pages-close svg {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
}
.pages-list { padding: 20px; }
.plan-list { padding: 20px; }
.plan-list[hidden] { display: none; }
.plan-row {
    display: grid;
    grid-template-columns: 28px 1fr auto;
    gap: 12px;
    align-items: baseline;
    padding: 15px 0;
    border-bottom: 1px solid rgba(34, 48, 58, 0.14);
}
.plan-number,
.plan-state {
    color: var(--board-ink-3);
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.plan-title { color: var(--board-ink); font-family: var(--font-ui); font-size: 14px; line-height: 1.45; }
.plan-row.is-current .plan-number,
.plan-row.is-current .plan-state { color: var(--accent); }
.plan-row.is-current .plan-title { font-weight: 600; }
.pages-empty {
    margin: 34vh 24px 0;
    color: var(--board-ink-3);
    font-family: var(--font-mono);
    font-size: 10px;
    line-height: 1.6;
    letter-spacing: 0.08em;
    text-align: center;
}
.page-card {
    margin: 0 0 24px;
    padding: 12px;
    background: #FAF8F2;
    border: 1px solid rgba(34, 48, 58, 0.18);
}
.page-card-meta {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 10px;
    color: var(--board-ink-2);
    font-family: var(--font-mono);
    font-size: 9px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.page-now { color: var(--accent); }
.page-preview {
    display: block;
    width: 100%;
    padding: 0;
    position: relative;
    height: 178px;
    overflow: hidden;
    background-color: #FAF8F2;
    background-image: var(--paper-grid);
    background-size: 30px 30px;
    border: 1px solid rgba(34, 48, 58, 0.16);
    color: inherit;
    text-align: left;
    cursor: pointer;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}
.page-preview:hover { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(194, 64, 42, 0.16); }
.page-preview:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.page-preview-board {
    width: 322.58%;
    max-width: none;
    min-height: 574px;
    padding: 28px;
    transform: scale(0.31);
    transform-origin: top left;
    pointer-events: none;
}
:where(body:not(.native-direct-board-player)) .page-preview[data-cols="1"] .board-col { min-width: 0; }
:where(body:not(.native-direct-board-player)) .page-preview[data-cols="2"] .board-col { flex-basis: calc(50% - 14px); }
.page-notes {
    padding-top: 10px;
    color: var(--board-ink-2);
    font-family: var(--font-ui);
    font-size: 12px;
    line-height: 1.55;
}
.page-notes p { margin: 0 0 7px; }
.page-notes p:last-child { margin-bottom: 0; }

.page-detail {
    position: absolute;
    inset: 0;
    z-index: 40;
    display: grid;
    place-items: center;
    padding: 24px;
    background: rgba(34, 48, 58, 0.38);
}
.page-detail[hidden] { display: none; }
.page-detail-dialog {
    display: flex;
    flex-direction: column;
    width: min(1120px, 100%);
    max-height: 100%;
    background: #FAF8F2;
    border: 1px solid rgba(34, 48, 58, 0.22);
    box-shadow: 0 18px 50px rgba(34, 48, 58, 0.2);
}
.page-detail-header {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: space-between;
    padding: 12px 18px;
    border-bottom: 1px solid rgba(34, 48, 58, 0.18);
    color: var(--board-ink-2);
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}
.page-detail-scroll { overflow-y: auto; }
.page-detail-paper {
    min-height: 574px;
    padding: 48px 40px 32px;
    overflow-x: hidden;
    background-color: #FAF8F2;
    background-image: var(--paper-grid);
    background-size: 30px 30px;
}
:where(body:not(.native-direct-board-player)) .page-detail-board[data-cols="1"] .board-col { min-width: 0; }
:where(body:not(.native-direct-board-player)) .page-detail-board[data-cols="2"] .board-col { flex-basis: calc(50% - 14px); }
.page-detail-notes {
    padding: 20px 40px 28px;
    border-top: 1px solid rgba(34, 48, 58, 0.18);
    color: var(--board-ink-2);
    font-family: var(--font-ui);
    font-size: 15px;
    line-height: 1.6;
}
.page-detail-notes p { margin: 0 0 10px; }
.page-detail-notes p:last-child { margin-bottom: 0; }

@media (max-width: 680px) {
    .board-footer { padding-left: 18px; padding-right: 18px; }
    .lesson-control { min-width: 0; padding: 7px 9px; }
    .lesson-control-copy > span { font-size: 8.5px; }
    .page-detail { padding: 12px; }
    .page-detail-paper { padding: 32px 22px 24px; }
    .page-detail-notes { padding: 18px 22px 24px; }
}

/* Primary CTA — a flat red-pen block in paper-colored mono caps, the same
   button .nb-featured-cta is on the shell pages. */
.primary-button {
    background: var(--accent);
    color: #FAF8F2;
    border: none;
    padding: 14px 26px;
    border-radius: 0;
    font-family: var(--font-mono);
    font-size: 11.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
    transition: filter 120ms ease;
}
.primary-button:hover:not(:disabled) {
    filter: brightness(1.08);
}
.primary-button:disabled {
    background: rgba(34, 48, 58, 0.10);
    color: rgba(34, 48, 58, 0.35);
    cursor: not-allowed;
}
/* Ghost: hairline border, quiet ink — the shell's .nb-button-secondary. */
.secondary-button {
    background: transparent;
    color: rgba(34, 48, 58, 0.7);
    border: 1px solid rgba(34, 48, 58, 0.25);
    padding: 13px 22px;
    border-radius: 0;
    font-family: var(--font-mono);
    font-size: 11.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
    transition: color 120ms ease, border-color 120ms ease;
}
.secondary-button:hover {
    color: #22303A;
    border-color: rgba(34, 48, 58, 0.5);
}

/* --- v3 hero-clip experiment (video directive) --- */
/* --- The coordinate plane (live-object surface) --- */
svg.board-plane {
    display: block;
    margin: 14px auto;
    max-width: 100%;
    height: auto;
    overflow: visible;
}
svg.board-plane .plane-tick {
    font-family: "IBM Plex Mono", monospace;
    font-size: 11px;
}
svg.board-plane .plane-label {
    font-family: "IBM Plex Mono", monospace;
    font-size: 13px;
    fill: #22303A;
}
svg.board-plane .plane-label-accent {
    fill: var(--accent);
    font-size: 12px;
}
/* The AI's pointing works on plane objects too. */
:where(body:not(.native-direct-board-player)) svg.board-plane g.state-highlighted circle,
:where(body:not(.native-direct-board-player)) svg.board-plane circle.state-highlighted {
    fill: var(--accent);
}
:where(body:not(.native-direct-board-player)) svg.board-plane g.state-highlighted line {
    stroke: var(--accent);
}
:where(body:not(.native-direct-board-player)) svg.board-plane g.state-dimmed,
:where(body:not(.native-direct-board-player)) svg.board-plane .state-dimmed {
    opacity: 0.3;
}
/* --- answer_plot: the graph-answer surface (question [ASK]s, founder 2026-07-17)
   While armed the plane invites clicking (crosshair + the same soft red glow
   the active answer_slot box gets); the student's marks are red —
   visually distinct from authored ink points. */
svg.board-plane.plane-answer-armed {
    cursor: crosshair;
    border-radius: 6px;
    box-shadow: 0 0 0 3px var(--accent-soft);
}
svg.board-plane .plane-student-point circle {
    fill: var(--accent);
}
svg.board-plane .plane-student-point circle:first-child {
    fill: none;                    /* the ink-outline ring around the red dot */
    stroke: var(--chalk-box);
}
svg.board-plane .plane-student-label {
    font-family: "IBM Plex Mono", monospace;
    font-size: 12px;
    fill: var(--accent);
}
svg.board-plane .plane-student-line line {
    stroke: var(--accent);
}

/* --- Bar model (tape diagram) --- */
svg.board-barmodel {
    display: block;
    margin: 14px auto;
    max-width: 100%;
    height: auto;
    overflow: visible;
}
svg.board-barmodel .barmodel-label {
    font-family: "IBM Plex Mono", monospace;
    font-size: 13px;
    fill: #22303A;
}
:where(body:not(.native-direct-board-player)) svg.board-barmodel g.state-highlighted rect {
    fill: rgba(194, 64, 42, 0.30);
}
:where(body:not(.native-direct-board-player)) svg.board-barmodel g.state-dimmed {
    opacity: 0.3;
}

/* --- Long division (the worked bracket) ---
   SVG owns the bracket, bars, and point-shift guides. Every mathematical glyph
   is a KaTeX-rendered HTML token inside the SVG's fixed place-value grid. */
svg.board-longdivision {
    display: block;
    margin: 14px auto;
    max-width: 100%;
    height: auto;
    overflow: visible;
}
.board-longdivision .ld-math-cell {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: visible;
    color: #22303A;
    font-size: 23px;
    line-height: 1;
}
.board-longdivision .ld-math-cell .katex {
    color: inherit;
    font-size: 1em;
}
/* The digit brought down from the dividend, and the 'R n' left over at the end:
   the two glyphs that are not products of the subtraction, in the red pen. */
svg.board-longdivision .ld-bring .ld-math-cell,
svg.board-longdivision .ld-remainder .ld-math-cell {
    color: var(--accent);
}
svg.board-longdivision .ld-sign .ld-math-cell {
    color: rgba(34, 48, 58, 0.75);
}
/* The decimal point rides a COLUMN BOUNDARY, not a column — it must never look
   like a digit's worth of space, so it keeps the digit face at the digit's ink. */
svg.board-longdivision .ld-point .ld-math-cell {
    color: #22303A;
}
/* The point-shift hops: the gesture that moved the points, in the accent like
   every other glyph on this figure that came from somewhere else. */
svg.board-longdivision .ld-shift path {
    fill: none;
    stroke: var(--accent);
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
}
svg.board-longdivision .ld-shift .ld-arrow-head {
    fill: var(--accent);
    stroke: none;
}
svg.board-longdivision .ld-shift-label .ld-math-cell {
    color: var(--accent);
    font-size: 10px;
}

/* --- The number line (live-object surface) --- */
svg.board-numline {
    display: block;
    margin: 14px auto;
    max-width: 100%;
    height: auto;
    overflow: visible;
}
svg.board-numline .numline-tick {
    font-family: "IBM Plex Mono", monospace;
    font-size: 11px;
}
svg.board-numline .numline-label {
    font-family: "IBM Plex Mono", monospace;
    font-size: 13px;
    fill: #22303A;
}
svg.board-numline .numline-label-accent {
    fill: var(--accent);
    font-size: 12px;
}
:where(body:not(.native-direct-board-player)) svg.board-numline g.state-highlighted circle {
    fill: var(--accent);
}
:where(body:not(.native-direct-board-player)) svg.board-numline g.state-dimmed {
    opacity: 0.3;
}
/* --- answer_numberline: click the live number line (question [ASK]s, founder
   2026-07-17). The student's marks are red, like the plane's — a value label
   above the dot, the boundary ring for inequalities, and two pulsing red
   direction arrows until one is chosen. */
svg.board-numline.numline-answer-armed { cursor: pointer; }
svg.board-numline .numline-student-label {
    font-family: "IBM Plex Mono", monospace;
    font-size: 12px;
    fill: var(--accent);
}
svg.board-numline .nl-dir-hotspot { cursor: pointer; }
svg.board-numline .nl-dir-hotspot .nl-dir-arrow { fill: var(--accent); opacity: 0.9; }
svg.board-numline .nl-dir-hotspot.nl-dir-pulse .nl-dir-arrow {
    animation: nl-dir-pulse 1.4s ease-in-out infinite;
}
svg.board-numline .nl-dir-hotspot.chosen .nl-dir-arrow { opacity: 1; animation: none; }
svg.board-numline .nl-dir-hotspot.dimmed .nl-dir-arrow { opacity: 0.25; animation: none; }
@keyframes nl-dir-pulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }

/* --- answer_order: drag the cards into order (question [ASK]s, founder
   2026-07-17). A board-flow row of ink-bordered white cards on the page;
   the lifted card scales up with a shadow and a red border while dragging. */
.board-answer-order {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    margin: 1rem 0;
}
.ao-card {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 3rem;
    min-height: 2.6rem;
    padding: 0.35rem 0.7rem;
    border: 1.5px solid var(--chalk-box);      /* ink hairline */
    border-radius: 6px;
    background: #FFFFFF;
    color: #22303A;
    font-family: var(--font-mono);
    font-size: 1.2rem;
    user-select: none;
    touch-action: none;                        /* pointer drag, not scroll */
    transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
}
.ao-card .katex { color: #22303A; }
.board-answer-order.ao-armed .ao-card { cursor: grab; }
.ao-card.ao-lift {
    transform: scale(1.08);
    box-shadow: 0 6px 18px rgba(34, 48, 58, 0.18);
    border-color: var(--accent);              /* red while lifted */
    cursor: grabbing;
}

/* --- answer_tap: click a labeled element on the figure (question [ASK]s, founder
   2026-07-17). A red ring pulses over each tappable label until one is chosen;
   the chosen ring goes solid. SVG matches get a circle; HTML matches an absolute
   div overlay. */
.tap-hotspot { cursor: pointer; }
svg circle.tap-hotspot {
    fill: transparent;
    stroke: var(--accent);
    stroke-width: 1.5;
    opacity: 0.5;
    animation: tap-pulse 1.6s ease-in-out infinite;
}
svg circle.tap-hotspot.tap-hotspot-chosen {
    opacity: 1;
    stroke-width: 2.5;
    animation: none;
}
.tap-hotspot-html {
    position: absolute;
    border: 1.5px solid var(--accent);
    border-radius: 6px;
    opacity: 0.5;
    cursor: pointer;
    animation: tap-pulse 1.6s ease-in-out infinite;
}
.tap-hotspot-html.tap-hotspot-chosen { opacity: 1; border-width: 2.5px; animation: none; }
@keyframes tap-pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 0.7; } }

/* --- Geometry figures (shape / angle / circle) --- */
svg.board-shape {
    display: block;
    margin: 14px auto;
    max-width: 100%;
    height: auto;
    overflow: visible;
}
svg.board-shape .shape-label {
    font-family: "IBM Plex Mono", monospace;
    font-size: 14px;
    fill: #22303A;
}
/* A single-letter figure/center name (circle "O", a vertex "P"). The serif
   display face makes a capital letter unmistakable from a digit — a mono "O"
   reads as "0" on a math board — so the conventional center-O notation stays
   legible. Dimension labels (r = 3, d = 10) keep the mono face above. */
svg.board-shape .shape-label-letter {
    font-family: var(--font-display), "Newsreader", Georgia, serif;
    font-size: 17px;
    font-style: italic;
}
svg.board-shape .shape-label-side {
    font-size: 13px;
    fill: rgba(34, 48, 58, 0.55);
}
svg.board-shape .shape-label-accent {
    fill: var(--accent);
    font-size: 13px;
}
:where(body:not(.native-direct-board-player)) svg.board-shape g.state-highlighted path,
:where(body:not(.native-direct-board-player)) svg.board-shape path.state-highlighted {
    stroke: var(--accent);
}
:where(body:not(.native-direct-board-player)) svg.board-shape g.state-highlighted circle,
:where(body:not(.native-direct-board-player)) svg.board-shape circle.state-highlighted {
    fill: var(--accent);
}
:where(body:not(.native-direct-board-player)) svg.board-shape g.state-dimmed,
:where(body:not(.native-direct-board-player)) svg.board-shape .state-dimmed {
    opacity: 0.3;
}

/* --- FOIL / product-pairing map --- */
svg.board-foil {
    display: block;
    margin: 14px auto;
    max-width: 100%;
    height: auto;
    overflow: visible;
}
svg.board-foil .foil-term,
svg.board-foil .foil-syntax {
    font-family: "IBM Plex Mono", monospace;
    font-size: 16px;
    fill: #22303A;
}
svg.board-foil .foil-syntax {
    fill: rgba(34, 48, 58, 0.55);
}
svg.board-foil .foil-product {
    font-family: "IBM Plex Mono", monospace;
    font-size: 13px;
    font-weight: 600;
}
svg.board-foil .foil-tag {
    font-family: "IBM Plex Mono", monospace;
    font-size: 10px;
    font-weight: 700;
    opacity: 0.85;
}
svg.board-foil .foil-result {
    font-family: "IBM Plex Mono", monospace;
    font-size: 16px;
    fill: #22303A;
}

/* v4 film-spine experiment: the board IS the video for a stretch of beats. */
.board-film {
    display: block;
    width: 100%;
    max-width: 880px;
    margin: 0 auto;
    border-radius: 6px;
}

.board-video {
    text-align: center;
    margin: 0.5rem 0;
}
.board-video video {
    max-width: 92%;
    border-radius: 6px;
    display: inline-block;
}

/* The dev beat-jump field: replaces window.prompt, which browsers block inside
   the tutor room's iframe. Sits exactly where the counter was. */
.beat-jump-input {
    width: 122px;
    padding: 2px 6px;
    border: 1px solid rgba(34, 48, 58, 0.3);
    border-radius: 2px;
    background: #FFFDF8;
    color: var(--chalk);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.beat-jump-input:focus { outline: none; border-color: var(--accent); }

/* Motion explains math, but it is never mandatory to operate the lesson.
   The JS renderers also skip their Web Animations under this preference; this
   block covers CSS reveals, board/panel travel, and camera smoothing. Native
   authored content handles the preference through its own animation code. */
@media (prefers-reduced-motion: reduce) {
    .player-body :where(:not(.direct-board-tape--native, .direct-board-tape--native *)),
    .player-body :where(:not(.direct-board-tape--native, .direct-board-tape--native *))::before,
    .player-body :where(:not(.direct-board-tape--native, .direct-board-tape--native *))::after {
        animation-duration: 0.001ms !important;
        animation-delay: 0ms !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
}
