/* templates.css — layout glue for template-authored pages (the responsive path).
   Thin by design: each template's look and interaction come from the vendored
   design system (js/vendor/_ds_bundle.js); this file only hosts the page inside
   the player's content zone (scrolling, padding). No content, no re-skinning. */

/* A template page fills the content zone and scrolls when its content is taller than the stage. */
.tpl-page {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* a step brighter than the chrome navy, so the stage reads as a panel above the chrome */
  background: var(--surface-stage);
  /* tight vertical padding so the fixed-canvas desktop composition fits with NO scroll (spacing
     only — never shrinking text); sides keep reasonable breathing room, esp. on mobile. */
  padding: var(--space-p6) var(--space-p28) var(--space-p10);
}

/* Unify the interaction with the stage. The component paints its root wrapper the chrome navy,
   which reads as a dark seam around the interaction. Match it to the stage surface so the stage
   content area and the interaction background are one lighter-blue panel. Also trim its 34px top
   padding to shift the interaction up. SCOPED per template — never global — so a template with a
   different surface (e.g. guidedStepper's white panel) is not forced navy. */
.tpl-page--category-match > div { background-color: var(--surface-stage) !important; padding-top: var(--space-p8) !important; }
.tpl-page--scorecard > div { padding-top: var(--space-p8) !important; }   /* .sc is transparent → the stage navy shows through */

/* Guided Steps: the handoff component is a full white surface with dark text. To sit it on the
   navy stage (matching category-match's lighter-than-chrome background) AND keep that text
   readable, present it as a white panel FILLING the stage with the navy showing as a framing
   margin — not shrink-wrapped into a small card. */
.tpl-page--guided-stepper { display: flex; flex-direction: column; padding: var(--space-p14) clamp(12px, 2vw, 28px) var(--space-p18); }
.tpl-page--guided-stepper > div {
  flex: 1; min-height: 0; width: 100%; max-width: var(--size-p1180); margin-inline: auto;
  border-radius: var(--radius-card); overflow: hidden; box-shadow: 0 8px 26px var(--ink-deep-a30);
}

/* ---- Guided Steps: responsive (tablet + phone) — the whole page fits with NO scrolling ---- */
@media (max-width: 1024px /* bp:tablet */) {
  /* Bound the page to the space between the header and the fixed bottom bar (56 + 96), so the
     stepper's own layout (content region + pinned step bar) fits without growing the document. */
  .tpl-page--guided-stepper {
    height: calc(100dvh - var(--rc-header-h, var(--chrome-flow-header-h)) - var(--chrome-flow-footer-h));
    min-height: 0; overflow: hidden;
    padding: var(--space-p10) clamp(10px, 2vw, 20px) var(--space-p12);
  }
  /* cap the step image so it never eats the vertical budget */
  .tpl-page--guided-stepper img { max-height: min(210px, 26vh) !important; }

  /* Prev/Next become CIRCLES that match the course-player nav buttons (.rc-nav): a
     52px circle carrying the SAME bold arrow SVG. font-size:0 hides the
     "Previous"/"Next" text; the SVG keeps its explicit 24px size. Class-based (not
     [aria-label=...]) so the locked-state label changes never drop the styling. */
  .tpl-page--guided-stepper .gs-nav {
    width: var(--chrome-pill-h) !important; height: var(--chrome-pill-h) !important; min-height: var(--chrome-pill-h) !important;
    padding: 0 !important; border-radius: var(--radius-circle) !important; flex-shrink: 0;
    justify-content: center !important; gap: 0 !important; font-size: 0 !important;
  }
  .tpl-page--guided-stepper .gs-nav svg { width: var(--size-p24) !important; height: var(--size-p24) !important; }

  /* tighten the step control bar (the div with the border-top seam) */
  .tpl-page--guided-stepper div[style*="border-top"] { padding: var(--space-p10) var(--space-p16) var(--space-p12) !important; }
}

/* When the row is too narrow for [ ‹  dots  › ] on one line, drop BOTH circles below the dots:
   force the dots (tablist) onto their own full-width row; the two circles wrap under it together.
   540px clears the widest the flanking row gets (~5 dots + two 52px circles) before it overflows. */
@media (max-width: 540px /* bp:stepper-wrap */) {
  .tpl-page--guided-stepper [role="tablist"] { order: -1; flex-basis: 100%; }
}

/* ---- Sequence (.tpl-page--sequence) — responsive reflow -------------------------------
   The component's visuals are inline styles from the vendored bundle, so the few structural
   overrides below need !important. Desktop (>1024px) is untouched. */
@media (max-width: 1024px /* bp:tablet */) {
  /* Zero the .tpl-page side padding here: `.seq-page` supplies its own, and the two were
     stacking — 28px + 16px per side ate 88px of a 390px phone, collapsing the action text to
     ~54px (3–4 lines). One owner of the horizontal gutter, not two. */
  .tpl-page--sequence { padding-left: 0 !important; padding-right: 0 !important; }
  .tpl-page--sequence .seq-page { padding: var(--space-p18) var(--space-p14) var(--space-p24) !important; }
  .tpl-page--sequence h2 { font-size: var(--type-h4) !important; }
}
/* ≤700px the component itself switches to LIST MODE (one reorderable list — top = first;
   no zones, no bank), so no grid reflow is needed here: the rows are compact horizontal
   cards sized inline by the component. */

/* Guided Steps reveal — a dismissible popover CARD (fixed overlay) rather than an inline panel that
   pushes the layout and forces scrolling. Backdrop or × closes it; a long reveal scrolls INSIDE the
   card, so the page never grows and "everything fits, no scrolling" holds at every screen size. */
.gs-reveal-overlay {
  position: fixed; inset: 0; z-index: var(--z-reveal-overlay);
  display: flex; align-items: center; justify-content: center; padding: var(--space-p20);
  background: var(--ink-deep-a45);
  font-family: var(--font-sans-system);
}
.gs-reveal-card {
  position: relative; width: 100%; max-width: var(--size-p460); max-height: 76vh; overflow: auto;
  background: var(--surface-stage);
  border: var(--border-hairline) solid var(--correct-a45); border-top: var(--border-w4) solid var(--correct);
  border-radius: var(--radius-r14); padding: var(--space-p24); box-shadow: 0 18px 50px var(--ink-deep-a55);
}
.gs-reveal-close {
  position: absolute; top: var(--space-p6); right: var(--space-p8); width: var(--size-p38); height: var(--size-p38);
  border: none; background: transparent; color: var(--white-a65); font-size: var(--type-stat); line-height: var(--lh-solid);
  cursor: pointer; border-radius: var(--radius-sm);
}
.gs-reveal-close:hover { color: var(--text-on-navy); }
.gs-reveal-label {
  font-weight: var(--fw-extrabold); font-size: var(--type-caption); letter-spacing: var(--track-caption); color: var(--correct-light);
  text-transform: uppercase; margin: 0 var(--space-p32) var(--space-p10) 0;
}
.gs-reveal-body { font-size: var(--type-copy); line-height: var(--lh-body); color: var(--white-a92); }

/* Guided Steps step image (author 2026-07-27, revised same day after diagnosis).
   The WRAPPER (.gs-step-media) owns the aspect-ratio, not the img — 8/3 on desktop
   (crop-free at the authored 1600x600 art, keeps the band short enough that the step
   body still fits without scrolling), 4/3 on mobile/tablet (those layouts ALREADY
   scroll per-step regardless of image height, so the crop can relax). The img just
   fills the wrapper (position:absolute; inset:0).
   WHY THE WRAPPER, NOT THE IMG DIRECTLY: an aspect-ratio on the img alone was silently
   clamped to a flat 210px on mobile/tablet by an OLDER, broader, pre-existing rule
   below (`.tpl-page--guided-stepper img { max-height: min(210px,26vh) !important }` —
   a "cap the image so it never eats the vertical budget" guard that predates this
   class and is left untouched here, since other images in that container may still
   need it). 8/3 never exceeded that cap so it went unnoticed; 4/3 did — confirmed by
   forcing max-height:none on the live element, which alone unlocked the correct
   234px height. The wrapper's OWN aspect-ratio isn't a "max-height", so that old rule
   can't touch it; `.gs-step-media .gs-step-img`'s max-height:none (below) then
   guarantees the img itself is never independently capped either — two classes beats
   the old rule's one-class-plus-type-selector specificity, so it reliably wins even
   though both carry !important. */
/* The band's shape is now AUTHORABLE per breakpoint (author 2026-07-29), because a fixed
   band shape cannot honour a protect-rect: with the desktop band at 8/3, the rects drawn
   over scenario-1-setup steps 2-3 and scenario-2-delivery-complete step 1 provably CANNOT
   FIT at any focal (authoring/scripts/frame_from_safe.py reports "CANNOT FIT at cover" for
   all three), so the subject was being cut no matter how the crop was tuned. At 4/3 all
   three fit. `config.aspect` / `step.aspect` publish --gs-media-aspect-* below; the media
   queries pick one, same reason as --gs-fit-* (a JS breakpoint read freezes at mount). */
.gs-step-media {
  aspect-ratio: var(--gs-media-aspect-desktop, 8 / 3);
  /* DESKTOP NEEDS A WIDTH CAP once the band is squarer than 8/3. The desktop stage is a fixed
     1080-tall canvas that does not scroll, and the band is width-driven — so a 4/3 band at the
     card's full width is ~645px tall and pushes the step title and body clean off the card
     (observed immediately on the first 4/3 desktop render). Capping the WIDTH keeps the aspect
     exactly 4/3 — so the protect-rect is still honoured — and simply draws the band smaller,
     centred, with the text back in view. Capping the height instead cannot work: `width: auto`
     on a block resolves to stretch rather than deriving from the aspect, so the box would just
     become wider than its ratio and crop the rect again. Default `none` = every existing 8/3
     page is untouched. */
  max-width: var(--gs-media-max-w, none);
  margin-inline: auto;
}
.gs-step-media .gs-step-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  max-height: none !important;
  /* WHICH BREAKPOINT'S CROP APPLIES is decided here, not in JS. _ds_bundle.js publishes
     --gs-fit-* / --gs-focal-* for all three platforms on the element; these rules choose
     one. The component does not re-render on resize, so a JS breakpoint read would freeze
     at whatever it was on mount — a media query simply cannot get out of date.
     The img's inline `object-fit: var(--gs-fit, cover)` reads whichever wins. */
  --gs-fit: var(--gs-fit-desktop, cover);
  --gs-focal: var(--gs-focal-desktop, center);
  object-fit: var(--gs-fit, cover);
}
/* SIDE-BY-SIDE STEPPER (its own template, 2026-09-19 - the stepper's component under a second page class): the panel a
   grid - the words at the left, the picture at the right spanning the rows the words take; the bundle's stacked order
   (media, title, body, callout, action) is unchanged, the grid places it. THE PICTURE IS SIZED BY THE HEIGHT (the same
   evening, Tyler: "do not limit the size of the guided stepper to an artificial horizontal width ... as wide as possible that
   still fits inside the player and the chrome"): the card is the container; the picture's height is the card's less the
   head, the padding and the step bar (a 4:3 picture on a wide card would otherwise run off the bottom), its width from its
   own ratio, capped at two thirds of the card; the words take everything left. */
.tpl-page--side-by-side-stepper .gs-panel { display: grid; grid-template-columns: minmax(var(--size-p300), 1fr) auto; grid-auto-rows: auto; column-gap: var(--space-p32); align-items: start; }
.tpl-page--side-by-side-stepper .gs-panel > .gs-step-media { grid-column: 2; grid-row: 1 / span 5; width: calc((100cqh - var(--size-p300) - var(--size-p20)) * var(--gs-media-aspect-desktop, 8 / 3)) !important; max-width: 65cqw; margin: 0; }   /* !important: the bundle writes width: 100% inline on the frame */
.tpl-page--side-by-side-stepper .gs-panel > :not(.gs-step-media) { grid-column: 1; }
.tpl-page--side-by-side-stepper .gs-panel > h3 { margin-top: 0; }
/* the card at its full width for the side-by-side (the bundle sizes the card to its 860 px content wrapper inline; the grid needs the
   room - the three anonymous boxes between the page and the panel are the card, its padding box and the wrapper) */
.tpl-page--side-by-side-stepper > div { width: 100%; max-width: none; container-type: size; }   /* the whole stage's width (Tyler, 2026-09-19: 'do not limit the size ... to an artificial horizontal width'); the stepper's own card keeps its 1180; the container the picture's height reads */
.tpl-page--side-by-side-stepper > div > div { width: 100%; box-sizing: border-box; }
.tpl-page--side-by-side-stepper > div > div > div { max-width: none !important; width: 100%; }
.tpl-page--side-by-side-stepper .gs-panel { width: 100%; }
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--side-by-side-stepper .gs-panel { display: block; }   /* stacked again where the layout scrolls */
  .tpl-page--side-by-side-stepper > div { container-type: normal; }   /* in flow here: a size container would collapse the card */
  .tpl-page--side-by-side-stepper .gs-panel > .gs-step-media { width: 100% !important; max-width: none; }   /* the picture at the full width again */
  .gs-step-media {
    aspect-ratio: var(--gs-media-aspect-tablet, var(--gs-media-aspect-desktop, 4 / 3));
    max-width: none;   /* these layouts already scroll, so the band takes the full width */
  }
  .gs-step-media .gs-step-img {
    --gs-fit: var(--gs-fit-tablet, var(--gs-fit-desktop, cover));
    --gs-focal: var(--gs-focal-tablet, var(--gs-focal-desktop, center));
  }
}
@media (max-width: 600px /* bp:phone */) {
  .gs-step-media {
    aspect-ratio: var(--gs-media-aspect-phone, var(--gs-media-aspect-tablet, var(--gs-media-aspect-desktop, 4 / 3)));
  }
  .gs-step-media .gs-step-img {
    --gs-fit: var(--gs-fit-phone, var(--gs-fit-desktop, cover));
    --gs-focal: var(--gs-focal-phone, var(--gs-focal-desktop, center));
  }
}

/* Shrink the oversized 36px title; keep the instruction (h2 + p — the ONLY reliable selector, as
   card bodies are also <p>) a compact subtitle directly under it (not orphaned). */
/* EXCLUSIONS ADDED 2026-09-06. This rule shrinks a template page's own oversized title, and
   it uses !important, so it captured any h2 inside .tpl-page. When .mc-prompt (the decision
   question, 24px) and .cmp-title (the completion title, 30px) were promoted from <div> to
   <h2> for WCAG 1.3.1, both were silently re-sized by this rule — the prompt grew 24->26px
   and reflowed the option row on a phone. They carry their own type scale, so they opt out
   rather than this rule losing its !important, which the pages it was written for still need. */
.tpl-page h2:not(.mc-prompt):not(.cmp-title) { font-size: var(--type-h2) !important; line-height: var(--lh-title) !important; }
/* Same exclusions, same reason (2026-09-06): .cmp-title is directly followed by <p class="cmp-body">,
   so promoting the title to <h2> would have pulled the completion screen's body text from its own
   18px down to this rule's 15px. The h2 this rule was written for is a template page's title. */
.tpl-page h2:not(.mc-prompt):not(.cmp-title) + p { margin: var(--space-p2) 0 var(--space-p6) !important; font-size: var(--type-small) !important; line-height: var(--lh-text) !important; }

/* Category Match cards — DESKTOP (fixed-canvas): the component centers its cards in a 1240px
   column with a fixed 3-column grid. Widen ~15% (still centered → cards spread out), 3 cols × 2
   rows. Overrides the component's inline styles; scoped to the stage; never the vendored bundle. */
.tpl-page [style*="max-width: 1240px"] { max-width: var(--size-p1426) !important; margin-inline: auto !important; }
.tpl-page ul[style*="grid-template-columns"] { grid-template-columns: repeat(3, 1fr) !important; }
/* Category Match with a scene band (config.scene): the items sit in ONE row of up
   to 4 so the page fits the stage without scrolling (author 2026-07-27). Class-based
   because the rule above clobbers any inline column count. */
.tpl-page ul.cm-items-row[style*="grid-template-columns"] { grid-template-columns: repeat(4, 1fr) !important; }

/* RESPONSIVE reflow below the fixed-canvas breakpoint (the player drops the fixed 1920 canvas at
   <1024px and flows to the viewport). The column goes fluid and the cards reflow: 3 → 2 (tablet)
   → 1 (phone, any). Breakpoints align with the player's fixed→responsive switch (RESPONSIVE_MIN). */
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page [style*="max-width: 1240px"] { max-width: 100% !important; }
  /* cue cards (the <ul>) → 2 columns; category-reference cards (the <div> grid) → 2 columns */
  .tpl-page ul[style*="grid-template-columns"],
  .tpl-page ul.cm-items-row[style*="grid-template-columns"] { grid-template-columns: repeat(2, 1fr) !important; }
  .tpl-page div[style*="grid-template-columns"] { grid-template-columns: repeat(2, 1fr) !important; }
}
@media (max-width: 640px /* bp:phone-grid */) {
  /* phone: single column for both cue cards and category-reference cards */
  .tpl-page ul[style*="grid-template-columns"],
  .tpl-page ul.cm-items-row[style*="grid-template-columns"],
  .tpl-page div[style*="grid-template-columns"] { grid-template-columns: 1fr !important; }
}

/* ===== Category Match — phone category-reference drawer (<= 600px) =================
   On phones the inline category cards are replaced by a collapsible right-edge drawer so the
   learner can consult the four categories while scrolling the cue cards. Rendered by
   category-match.js from the SAME config.categories (no duplicated content); overlay only — it
   never changes page width or the interaction. Hidden entirely above the phone breakpoint. */
.cm-cat-panel { display: none; }
/* Category Match scene band (config.scene): the scenario image shows at EVERY
   breakpoint (author 2026-07-27). Desktop/tablet keep the 3/4-image + 1/4-reference
   band; on the phone (<=600px) the band survives the structural reference-hide below
   and shows the IMAGE full-width — only the inline reference card hides, because the
   Categories drawer owns the categories there. */
@media (max-width: 600px /* bp:phone */) {
  .tpl-page--category-match h2 + p + div.cm-scene-band { display: flex !important; }
  .cm-scene-band .cm-scene-ref { display: none !important; }   /* inline display:flex needs the bang */
  /* per-screen override (author 2026-07-27): the phone-only squarer crop (see
     scene.mobileAspect in _ds_bundle.js) — desktop/tablet keep scene.aspect's default.
     !important: the base ratio is an INLINE style on the same element (bundle), which
     always wins over a plain class rule regardless of specificity. */
  .cm-scene-card.cm-scene-card--mobile-aspect { aspect-ratio: var(--cm-scene-mobile-aspect) !important; }
}

@media (max-width: 600px /* bp:phone */) {
  /* replace the inline "CATEGORY REFERENCE" white box (the div right after the instruction) */
  .tpl-page--category-match h2 + p + div { display: none !important; }

  /* the sliding panel: fixed between the (measured) header and the viewport bottom, off-screen
     right when closed. pointer-events:none so the closed/empty area never blocks the page. */
  .cm-cat-panel {
    display: block; position: fixed; z-index: var(--z-template-drawer);
    top: var(--cm-header-h, var(--chrome-flow-header-h)); bottom: 0; right: 0;
    width: min(88vw, 360px);
    transform: translateX(100%);
    transition: transform var(--dur-t240) var(--ease-standard);
    overflow: visible;            /* let the tab stick out past the panel's left edge */
    pointer-events: none;
  }
  .cm-cat-panel.is-open { transform: translateX(0); }

  /* the tab: rides the panel's left edge, so it peeks when closed and sits on the drawer edge
     when open — the same button toggles both (aria-expanded on it). Minimal footprint. */
  .cm-cat-tab {
    position: absolute; left: var(--space-n40); top: var(--space-p16); width: var(--size-p40);
    margin: 0; border: 0; padding: var(--space-p14) var(--space-p8);
    /* Deep Navy on Signal Blue = 5.13:1; white would be 2.88:1, failing WCAG 1.4.3 AND 1.4.11 */
    background: var(--signal); color: var(--text-on-signal);
    font: var(--fw-bold) var(--type-label)/var(--lh-solid) system-ui, sans-serif; letter-spacing: var(--track-tab);
    writing-mode: vertical-rl; text-orientation: mixed;
    border-radius: var(--radius-r-10-0-0-10); box-shadow: -3px 0 10px var(--black-a28);
    cursor: pointer; pointer-events: auto;
  }
  .cm-cat-tab:focus-visible { outline: var(--border-focus) solid var(--white); outline-offset: 2px; }

  /* the drawer body: fills the panel, scrolls when taller than the viewport */
  .cm-cat-drawer {
    height: 100%; overflow-y: auto; pointer-events: auto;
    background: var(--surface-card); box-shadow: -8px 0 24px var(--navy-d8-a35);
    padding: var(--space-p18) var(--space-p18) var(--space-p24);
  }
  .cm-cat-drawer-head { margin: 0 0 var(--space-p12); font: var(--fw-extrabold) var(--type-body-md)/var(--lh-title) system-ui, sans-serif; color: var(--text-heading); }
  .cm-cat-drawer-head:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 3px; border-radius: var(--radius-r4); }
  .cm-cat-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-p12); }
  /* color is a per-category ACCENT only; meaning is always the label + description text */
  .cm-cat-item { border-left: var(--border-w4) solid var(--cm-cat-color, var(--text-heading)); background: var(--navy-a04); border-radius: var(--radius-sm); padding: var(--space-p10) var(--space-p12); }
  .cm-cat-item-name { display: block; font: var(--fw-extrabold) var(--type-small)/var(--lh-title) system-ui, sans-serif; color: var(--cm-cat-color, var(--text-heading)); }
  .cm-cat-item-def  { display: block; margin-top: var(--space-p4); font: var(--fw-medium) var(--type-caption)/var(--lh-text-loose) system-ui, sans-serif; color: var(--ink); }
}

@media (prefers-reduced-motion: reduce) {
  .cm-cat-panel { transition: none; }
}

/* ===== Completion — course-complete + certificate (LAST screen) ===================
   Demo-only template reproduced faithfully from the handoff Completion design: a
   centered card with a green check, eyebrow, course title, message, and a
   "Download certificate" button. Content via config (completion.js). ============== */
.tpl-page--completion {
  display: flex; align-items: center; justify-content: center;
  min-height: 100%; padding: clamp(24px, 5vw, 64px);
}
.cmp-card {
  background: var(--surface-card); border: var(--border-hairline) solid var(--border); border-radius: var(--radius-r20);
  padding: clamp(36px, 6vw, 56px) clamp(28px, 6vw, 64px);
  text-align: center; max-width: var(--size-p720); width: 100%;
  box-shadow: 0 20px 50px var(--black-a18);
}
.cmp-check {
  width: var(--size-p84); height: var(--size-p84); border-radius: var(--radius-circle); margin: 0 auto var(--space-p24);
  background: var(--correct-a10); border: var(--border-w2) solid var(--correct); color: var(--correct);
  display: flex; align-items: center; justify-content: center; font: var(--fw-extrabold) var(--type-mark-xl)/var(--lh-solid) var(--font-ui);
}
.cmp-eyebrow {
  font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-eyebrow); text-transform: uppercase;
  /* WCAG 1.4.3 (2026-09-06): --signal on white is 2.88:1 and this is 15px, so it needs 4.5:1.
     Darkened along the same hue rather than swapped to navy, which would flatten the eyebrow
     into the title below it. #0076A0 on white = 5.12:1. */
  color: var(--signal-text); margin-bottom: var(--space-p8);
}
/* `margin: 0 0 6px` since 2026-09-06: this is an <h2> (WCAG 1.3.1) and a heading brings a
   browser margin-top the div never had; the 6px below it is the original spacing. */
.cmp-title { font: var(--fw-extrabold) var(--type-h1)/var(--lh-heading) var(--font-ui); color: var(--text-heading); letter-spacing: var(--track-heading); margin: 0 0 var(--space-p6); }
.cmp-body { font: var(--fw-regular) var(--type-body-md)/var(--lh-prose-loose) var(--font-ui); color: var(--secondary); margin: var(--space-p12) 0 var(--space-p28); }
.cmp-btn {
  border: none; border-radius: var(--radius-pill); padding: var(--space-p15) var(--space-p40); cursor: pointer;
  font: var(--fw-extrabold) var(--type-body)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-dialogue);
  background: var(--signal); color: var(--text-on-signal);
}
.cmp-btn:hover { background: var(--signal-hover); }
.cmp-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--signal-a55); }
/* Link mode (config.button.href — the web release's final-quiz handoff): same visual as the
   button, but an <a>, which needs its inline/underline defaults reset to match. */
a.cmp-btn { display: inline-block; text-decoration: none; text-align: center; }

/* ===== Scorecard — end-of-course results summary ==================================
   Demo-only template reproduced faithfully from design tokens (never the vendored
   bundle). Two columns: left = pass/fail badge + big N/M score + accuracy/passing;
   right = the per-decision review list. Data comes entirely from published scores
   (scoring.js). DESKTOP layout here; responsive reflow is the final build step. */
.sc {
  display: flex;
  gap: var(--space-8);
  align-items: flex-start;
  padding: var(--space-p24) var(--space-p32) var(--space-p32);
  min-height: 0;
}

/* Left summary card */
.sc-summary {
  width: var(--size-p420);
  flex-shrink: 0;
  background: var(--surface-card);
  border: var(--border-hairline) solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-p40);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  justify-content: center;
  gap: var(--space-p6);
  box-shadow: var(--shadow-card);
}
.sc-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-p10);
  font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-badge);
  text-transform: uppercase;
  border-radius: var(--radius-pill);
  border: var(--border-w1-5) solid;
  padding: var(--space-p8) var(--space-p20);
  margin-bottom: var(--space-p12);
}
.sc-badge.is-pass { color: var(--correct); border-color: var(--correct); background: var(--correct-wash); }
.sc-badge.is-fail { color: var(--incorrect); border-color: var(--incorrect); background: var(--incorrect-wash); }
.sc-badge-dot {
  width: var(--size-p22); height: var(--size-p22); border-radius: var(--radius-circle);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--type-label); color: var(--text-on-navy);
}
.sc-badge.is-pass .sc-badge-dot { background: var(--correct); }
.sc-badge.is-fail .sc-badge-dot { background: var(--incorrect); }
.sc-score { font: var(--fw-black) var(--type-display-score)/var(--lh-solid) var(--font-ui); color: var(--text-heading); letter-spacing: var(--track-score); }
.sc-score .sc-denom { color: var(--disabled-btn); font-weight: var(--fw-extrabold); }
.sc-score-label { font: var(--fw-medium) var(--type-body-md)/var(--lh-text-tight) var(--font-ui); color: var(--secondary); }
.sc-stats {
  display: flex; gap: var(--space-p40); justify-content: center; align-items: stretch;
  width: 100%; margin-top: var(--space-p24); padding-top: var(--space-p24);
  border-top: var(--border-hairline) solid var(--page);
}
.sc-stat-val { font: var(--fw-extrabold) var(--type-stat)/var(--lh-solid) var(--font-ui); color: var(--signal); }
.sc-stat-val.is-navy { color: var(--text-heading); }
.sc-stat-label { font: var(--fw-semibold) var(--type-label)/var(--lh-title) var(--font-ui); color: var(--muted); margin-top: var(--space-p2); }
.sc-stat-sep { width: var(--border-hairline); background: var(--page); }

/* Right review list */
.sc-review { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.sc-review-title {
  font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-eyebrow); text-transform: uppercase;
  color: var(--signal); margin-bottom: var(--space-p16);
}
.sc-rows { display: flex; flex-direction: column; gap: var(--space-p14); }
.sc-row {
  display: flex; align-items: center; gap: var(--space-p18);
  background: var(--surface-card);
  border: var(--border-w1-5) solid var(--border);
  border-radius: var(--radius-option);
  padding: var(--space-p18) var(--space-p22);
}
.sc-row.is-wrong { background: var(--incorrect-wash); border-color: var(--incorrect); }
.sc-mark {
  width: var(--size-p34); height: var(--size-p34); flex-shrink: 0; border-radius: var(--radius-circle);
  display: flex; align-items: center; justify-content: center;
  font: var(--fw-extrabold) var(--type-body)/var(--lh-solid) var(--font-ui);
  color: var(--text-on-accent); background: var(--correct);
}
.sc-row.is-wrong .sc-mark { background: var(--incorrect); }
.sc-q { flex: 1; min-width: 0; }
.sc-q-text { font: var(--fw-bold) var(--type-body-md)/var(--lh-text-tight) var(--font-ui); color: var(--ink); margin-bottom: var(--space-p3); }
.sc-q-chose { font: var(--fw-regular) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--secondary); }
.sc-q-correct { font: var(--fw-semibold) var(--type-caption)/var(--lh-text) var(--font-ui); color: var(--correct); margin-top: var(--space-p3); }
/* The withheld explanation (`immediateFeedback: false`). Set apart with a rule rather than a
   colour, because it is prose that has to be READ, not a verdict to be scanned — and because
   the row already carries green/red for the outcome. */
.sc-q-feedback {
  font: var(--fw-regular) var(--type-caption)/var(--lh-prose) var(--font-ui); color: var(--secondary);
  margin-top: var(--space-p7); padding-left: var(--space-p11); border-left: var(--border-w3) solid var(--border);
}
.sc-row.is-wrong .sc-q-feedback { border-left-color: var(--incorrect-a45); }
.sc-verdict {
  flex-shrink: 0;
  font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-title); color: var(--correct);
}
.sc-row.is-wrong .sc-verdict { color: var(--incorrect); }
.sc-empty {
  margin: auto; max-width: var(--size-p520); text-align: center;
  color: var(--text-on-navy);
  font: var(--fw-medium) var(--type-copy)/var(--lh-prose-loose) var(--font-ui);
}

/* FAIL STATE — what happens next, below the score inside the verdict card. Rendered only
   when graded work exists AND the learner is under the pass mark, so a passing scorecard is
   untouched. Separated from the stats by a rule rather than a coloured panel: the badge
   above already carries the red, and a second alarm would shout at someone who has just
   been told they failed. */
.sc-fail {
  margin-top: var(--space-p22); padding-top: var(--space-p20);
  border-top: var(--border-hairline) solid var(--border);
  width: 100%; text-align: center;
}
.sc-fail-title {
  font: var(--fw-extrabold) var(--type-lead)/var(--lh-snug) var(--font-ui);
  color: var(--incorrect); margin-bottom: var(--space-p8);
}
.sc-fail-body {
  font: var(--fw-regular) var(--type-small)/var(--lh-body) var(--font-ui);
  color: var(--secondary); margin: 0 0 var(--space-p18);
}
.sc-fail-btn {
  font: var(--fw-bold) var(--type-small)/var(--lh-solid) var(--font-ui);
  color: var(--text-on-accent); background: var(--signal);
  border: none; border-radius: var(--radius-btn);
  padding: var(--space-p14) var(--space-p28); cursor: pointer;
}
.sc-fail-btn:hover { background: var(--signal-hover); }
.sc-fail-btn:active { background: var(--signal-pressed); }

/* RESPONSIVE reflow (the "final build step" above). At/below the tablet breakpoint the two
   columns stack: the overall-result card becomes a full-width banner on top, and the decision
   review moves underneath — its rows in a 2-up grid on tablet, a single column on phone.
   Breakpoints match the other template reflows above (1024 → tablet, 640 → phone). */
@media (max-width: 1024px /* bp:tablet */) {
  .sc { flex-direction: column; align-items: stretch; padding: var(--space-p20) var(--space-p24) var(--space-p28); }
  .sc-summary { width: auto; }                             /* overall result → full-width banner on top */
  .sc-rows { display: grid; grid-template-columns: repeat(2, 1fr); }   /* answers → two columns underneath */
}
@media (max-width: 640px /* bp:phone-grid */) {
  .sc { padding: var(--space-p16) var(--space-p16) var(--space-p24); }
  .sc-rows { grid-template-columns: 1fr; }                 /* phone → single column */
}

/* ===== Accordion — "Explore" two-panel reveal ====================================
   Demo-only template reproduced faithfully from design tokens (never the vendored
   bundle). Left = an illustration that cross-fades to the open panel; right = a
   single-open vertical accordion (numbered rows, +/- toggle). Content + images
   come entirely from the screen JSON (accordion.js). Colors mirror the handoff:
   DEEP #0E1C42 media panel, signal-blue accents, white text on the navy stage. */
.tpl-page--accordion > div { padding-top: var(--space-p8); }   /* .acc is transparent → the stage navy shows through */
.acc {
  /* ONE phase of the panel-change choreography; two phases = the advance-click
     clip runtime (ui-sounds.js / accordion.js PHASE_MS — keep in sync) */
  --acc-phase: var(--dur-phase);
  display: flex;
  gap: 0;
  min-height: 0;
  height: 100%;
  background: var(--bar-deep);
  border-radius: var(--radius-card);
  overflow: hidden;
  border: var(--border-hairline) solid var(--white-a10);
}

/* Left illustration panel — swaps with the open row */
.acc-media {
  width: 46%;
  flex-shrink: 0;
  background: var(--bar-deep);
  border-right: var(--border-hairline) solid var(--white-a10);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-p40);
}
.acc-media-frame {
  width: 100%;
  aspect-ratio: var(--acc-ar, 8 / 3);   /* matches the authored scene art (1600×600); wide, subject left-anchored - a course may set
                                           config.imageAspect (the Precision Tork course: "4 / 3" - PC-only, the frame fills the left; 2026-09-16) */
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-option);
  background: var(--grey-6);
  box-shadow: inset 0 2px 22px var(--navy-a14);
  border: var(--border-hairline) solid var(--grey-10);
  /* ADVANCE-CLICK contract (author 2026-07-26): the panel change is TWO PHASES of
     --acc-phase each, totalling the advance-click clips' runtime (~740ms, see
     ui-sounds.js + accordion.js PHASE_MS — change them together).
       phase 1: rows shift (grid-rows below) while the OLD image fades out
       phase 2: the NEW image fades in together with the new body text
     Row headers never fade — only the panel content that changes. */
  animation: mvImageFade var(--acc-phase, var(--dur-phase)) var(--ease-in-out);   /* keyframes in player.css */
}
.acc-media-frame.is-leaving { animation: accMediaOut var(--acc-phase, var(--dur-phase)) var(--ease-in-out) both; }
/* THE MARKS (2026-09-19, Tyler: "a circle around each of them that pulsates in turquoise"): a panel's marks as rings in the
   signal colour over its picture. .acc-marks is the DRAWN picture's box inside the frame (accordion.js drawnBox - the frame
   clips the overflow of a cover-fit picture, so a mark past the edge is clipped with it); each .acc-mark is centred on its
   point; the ring breathes between the alpha and full, scaling a little - the grip ring's pulse (.cc-grip-ring). */
.acc-marks { position: absolute; pointer-events: none; z-index: var(--z-l1); }
.acc-mark { position: absolute; transform: translate(-50%, -50%); }
.acc-mark-ring { position: absolute; inset: 0; border-radius: var(--radius-circle); border: var(--border-w2) solid var(--signal); background: var(--signal-a12); box-shadow: 0 0 0 var(--border-w2) var(--signal-a20); animation: accMarkPulse var(--dur-t600) var(--ease-in-out) infinite alternate; }
@keyframes accMarkPulse { from { transform: scale(1); opacity: var(--alpha-85); } to { transform: scale(1.12); opacity: 1; } }
.acc-body-pic { position: relative; display: none; }   /* the inline copy's box on tablet / phone: shown with the inline picture below */
@keyframes accMediaOut { from { opacity: 1; } to { opacity: 0; } }
.acc-media-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* THE PANEL CAROUSEL (2026-09-19): a panel's `images`, one at a time in the frame - the whole picture (contain, a binder page
   reads), a bar along the bottom with the arrows, the caption and the dots, on the caption plate's gradient */
.acc-cyc { position: absolute; inset: 0; }
.acc-cyc-img.acc-media-img { object-fit: contain; background: var(--grey-6); }
.acc-cyc-bar { position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: flex-end; gap: var(--space-p10); padding: var(--space-p34) var(--space-p12) var(--space-p12); background: var(--grad-caption-plate); }
.acc-cyc-mid { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-p8); }
.acc-cyc-caption { font: var(--fw-bold) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-on-navy); text-align: center; }
.acc-cyc-dots { display: flex; gap: var(--space-p8); justify-content: center; align-items: center; }
.acc-cyc-dot { width: var(--size-p9); height: var(--size-p9); padding: 0; border-radius: var(--radius-circle); cursor: pointer; border: var(--border-w1-5) solid var(--white-a55); background: transparent; }
.acc-cyc-dot.is-current { background: var(--signal); border-color: var(--signal); }
.acc-cyc-dot:focus-visible, .acc-cyc-arrow:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.acc-cyc-arrow { flex: 0 0 auto; width: var(--size-p38); height: var(--size-p38); border-radius: var(--radius-circle); border: var(--border-w1-5) solid var(--border); background: var(--surface-card-a94); color: var(--text-heading); font: var(--fw-bold) var(--type-h4)/var(--lh-solid) var(--font-ui); cursor: pointer; display: grid; place-items: center; }
.acc-cyc-arrow:disabled { opacity: var(--alpha-32); cursor: default; }
/* the inline copy (the narrow layout): the picture in flow, the bar over its foot */
.acc-cyc.is-inline { position: relative; inset: auto; max-width: var(--size-p560); margin: var(--space-p4) auto var(--space-p16); }
.acc-cyc.is-inline .acc-body-img { margin: 0; }

/* Right accordion column */
.acc-list-col {
  flex: 1;
  min-width: 0;
  padding: var(--space-p40) var(--space-p56);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.acc-eyebrow {
  font: var(--fw-extrabold) var(--type-body)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--signal);
}
.acc-heading {
  font-family: var(--font-display);
  font-weight: var(--fw-extrabold); font-size: var(--type-h1-lg); line-height: var(--lh-heading);
  color: var(--text-on-navy); margin: var(--space-p6) 0 var(--space-p20);
}
/* authored line breaks in panel bodies render as-is (e.g. a formula on its own line,
   2026-07-26); single-paragraph bodies are unaffected. The OPEN panel's body text
   fades in during PHASE 2 (delayed one phase, invisible through the row shift via
   fill-mode both) — together with the new image, per the advance-click contract. */
.acc-body-text { white-space: pre-line; }
.acc-body-wrap.is-open .acc-body-text {
  animation: mvImageFade var(--acc-phase, var(--dur-phase)) var(--ease-in-out) var(--acc-phase, var(--dur-phase)) both;
}
/* optional instruction line under the heading (config.subtitle, 2026-07-26) */
.acc-subtitle {
  margin: var(--space-n10) 0 var(--space-p18);
  font: var(--fw-regular) var(--type-copy)/var(--lh-prose) var(--font-ui);
  color: var(--white-a82);
}
.acc-list { display: flex; flex-direction: column; }
.acc-item { border-bottom: var(--border-hairline) solid var(--white-a12); }
/* PHASE 1 row shift: every body lives in a grid-rows wrapper (0fr closed, 1fr
   open) so the closing and opening panels animate against each other over one
   phase. Closed wraps are `inert` (renderer) — out of tab order and the a11y
   tree, the job the old `hidden` attribute did. */
.acc-body-wrap {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows var(--acc-phase, var(--dur-phase)) var(--ease-in-out);
}
.acc-body-wrap.is-open { grid-template-rows: 1fr; }
.acc-head {
  width: 100%; text-align: left; background: transparent; border: none; cursor: pointer;
  padding: var(--space-p20) var(--space-p4); display: flex; align-items: center; gap: var(--space-p16);
}
.acc-num {
  width: var(--size-p30); height: var(--size-p30); flex-shrink: 0; border-radius: var(--radius-circle);
  display: flex; align-items: center; justify-content: center;
  font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui);
  background: var(--white-a10); color: var(--white-a70);
}
/* Deep Navy on Signal Blue = 5.13:1; white would be 2.88:1, failing WCAG 1.4.3 AND 1.4.11 */
.acc-item.is-open .acc-num { background: var(--signal); color: var(--text-on-signal); }
.acc-title { flex: 1; font: var(--fw-bold) var(--type-h5)/var(--lh-snug) var(--font-ui); color: var(--text-on-navy); }
.acc-toggle {
  font-size: var(--type-h1); font-weight: var(--fw-regular); color: var(--signal);
  line-height: var(--lh-solid); width: var(--size-p24); text-align: center; flex-shrink: 0;
}
.acc-body {
  padding: 0 var(--space-p4) 0 var(--space-p46);
  font: var(--fw-regular) var(--type-body-md)/var(--lh-prose-loose) var(--font-ui);
  color: var(--white-a80);
  /* grid-rows collapse plumbing: the wrapper track animates 0fr<->1fr; the body
     itself must be clippable for that to read as a slide. The old mvModalIn pop
     is retired — the choreography above owns the entrance now.
     CLOSED = zero vertical padding, and that is load-bearing: a border-box can
     never be shorter than its own padding, so a closed body with padding-bottom
     22px holds the track open 22px and the first line of text peeks through
     (author bug report). The bottom padding belongs to the OPEN state only and
     transitions with the row shift, so it reads as part of the same motion. */
  overflow: hidden; min-height: 0;
  transition: padding-bottom var(--acc-phase, var(--dur-phase)) var(--ease-in-out);
}
.acc-body-wrap.is-open .acc-body { padding-bottom: var(--space-p22); }
.acc-head:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; border-radius: var(--radius-sm); }

/* The per-row inline image is desktop-hidden: on desktop the shared left panel is the
   illustration, so this copy never shows above 1024px. */
.acc-body-img { display: none; }

/* RESPONSIVE reflow (matches the other template reflows: 1024 → tablet/phone). The shared
   top illustration is disconnected from whichever row you tapped (and scrolls off-screen as
   you open lower rows), so HIDE it and show each panel's image INSIDE its own expanded body
   instead — the illustration then stays with the expanded item. */
@media (max-width: 1024px /* bp:tablet */) {
  .acc { flex-direction: column; height: auto; }
  .acc-media { display: none; }
  .acc-list-col { padding: var(--space-p24) clamp(16px, 4vw, 40px) var(--space-p28); }
  .acc-heading { font-size: var(--type-h2); }
  .acc-title { font-size: var(--type-body-md); }
  /* Clicking a header scrolls it into view (accordion.js, author 2026-07-27) so
     it anchors near the top and the expanding image/text reveal by scrolling
     down from there. scroll-margin-top keeps scrollIntoView from landing the
     header UNDER the fixed viewport-anchored chrome header (--rc-header-h,
     player.css .rc-shell) with a little breathing room past it. */
  .acc-head { scroll-margin-top: calc(var(--rc-header-h, var(--chrome-flow-header-h)) + 12px); }
  /* Symmetric horizontal padding here so the image centers on the FULL column. The body's
     title-indent (46px) moves onto the text only (.acc-body-text) — otherwise the lopsided
     46px-left / 4px-right box would push the "centered" image ~21px to the right. */
  /* zero vertical padding when closed (same peek-guard as desktop); the is-open
     rule above restores the 22px bottom, transitioned with the row shift */
  .acc-body { font-size: var(--type-body); padding: 0; }
  .acc-body-text { padding-left: var(--space-p46); }
  .acc-body-pic { display: block; width: 100%; max-width: var(--size-p560); margin: var(--space-p4) auto var(--space-p16); }   /* a marked panel's box takes the picture's sizing; the picture fills it */
  .acc-body-pic .acc-body-img { width: 100%; max-width: none; margin: 0; }
  .acc-body-img {
    display: block;
    width: 100%; max-width: var(--size-p560);
    /* THE PICTURE KEEPS ITS OWN SHAPE HERE (author 2026-07-29: "way too cropped and
       zoomed in on tablet and on mobile — it should keep the aspect ratio it was printed
       in and just zoom out to fit").
       This used to force `aspect-ratio: 4/3; object-fit: cover` for headroom (2026-07-27).
       But the panel art is authored at 8/3, so squeezing a wide plate into a 4/3 box and
       cropping to fill threw away most of each side. `auto` + `contain` lets the box take
       the image's natural ratio: nothing is cut, the whole plate is simply smaller — which
       is the right trade on a layout that already scrolls. */
    aspect-ratio: auto; height: auto; object-fit: contain;
    border-radius: var(--radius-option);
    background: var(--grey-6); border: var(--border-hairline) solid var(--grey-10);
    box-shadow: inset 0 2px 22px var(--navy-a14);
    margin: var(--space-p4) auto var(--space-p16);   /* truly centered now that the box is symmetric */
  }
  /* tablet/phone: the inline image IS the picture, so it takes phase 2 with the text */
  .acc-body-wrap.is-open .acc-body-img {
    animation: mvImageFade var(--acc-phase, var(--dur-phase)) var(--ease-in-out) var(--acc-phase, var(--dur-phase)) both;
  }
}

/* Reduced motion: hard-cut, no fades, no row-shift transition (the renderer also
   skips the swap state entirely). */
@media (prefers-reduced-motion: reduce) {
  .acc-media-frame, .acc-media-frame.is-leaving, .acc-body, .acc-body-img, .acc-body-text,
  .acc-body-wrap.is-open .acc-body-text, .acc-body-wrap.is-open .acc-body-img { animation: none; }
  .acc-body-wrap, .acc-body { transition: none; }
  .acc-mark-ring { animation: none; }
}

/* ===== Multiple Choice — single-answer question ===================================
   Demo-only template reproduced faithfully from design tokens (never the vendored
   bundle). Optional scenario rail (DEEP navy) + scene image with a thought bubble,
   a NAVY prompt bar, and a row of white answer cards. Select → Submit → feedback
   modal → retry-until-correct. Scorable (scoring.js). DESKTOP layout here; the
   responsive reflow is the final build step below. Tokens: DEEP #0E1C42,
   NAVY #132554, signal #00A3DD, ink #16203A, correct #12A66A, incorrect #E5463E. */
.tpl-page--multiple-choice { padding: var(--space-p8); position: relative; }   /* anchors the intro modal */

/* ===== Multiple Choice: intro narration modal (config.intro) ================
   The narrator stands in the lecture-accordion ROOM (light-navy wall + floor
   gradient) while the scenario + observations fall in beside her. Desktop and
   tablet: text LEFT / figure RIGHT. Phone: text above, figure below. */
.mc-intro-scrim {
  position: absolute; inset: 0; z-index: var(--z-modal);
  background: var(--navy-d10-a72);
  display: flex; align-items: center; justify-content: center;
  animation: mvModalIn var(--dur-t240) var(--ease-standard);
}
.mc-intro-modal {
  position: relative;
  width: min(1040px, 92%); height: min(620px, 90%);
  background: var(--navy-l3);
  border: var(--border-hairline) solid var(--on-navy-rule);
  border-radius: var(--radius-r20); overflow: hidden;
  display: flex;
}
.mc-intro-panel {
  flex: 1; min-width: 0; padding: var(--space-p42) var(--space-p46) var(--space-p30);
  display: flex; flex-direction: column; gap: var(--space-p16); overflow: auto;
}
.mc-intro-eyebrow {
  display: block;
  font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-intro-eyebrow); text-transform: uppercase;
  color: var(--signal);
  margin-bottom: var(--space-p8);
}
.mc-intro-item + .mc-intro-item .mc-intro-eyebrow { margin-top: var(--space-p10); }
.mc-intro-text { margin: 0; font: var(--fw-regular) var(--type-lead)/var(--lh-prose) var(--font-ui); color: var(--white-a95); }
.mc-intro-obs {
  display: flex; gap: var(--space-p12); align-items: flex-start;
  font: var(--fw-regular) var(--type-copy)/var(--lh-prose) var(--font-ui); color: var(--white-a88);
}
.mc-intro-dot {
  flex: 0 0 auto; width: var(--size-p10); height: var(--size-p10); margin-top: var(--space-p7); border-radius: var(--radius-circle);
  border: var(--border-w2) solid var(--signal);
}
.mc-intro-item { opacity: 0; transform: translateY(10px); transition: opacity var(--dur-t450) var(--ease-default), transform var(--dur-t450) var(--ease-default); }
.mc-intro-item.is-in { opacity: 1; transform: none; }
.mc-intro-btn {
  margin-top: auto; align-self: flex-start;
  border: none; border-radius: var(--radius-pill); padding: var(--space-p15) var(--space-p40); cursor: pointer;
  font: var(--fw-extrabold) var(--type-body)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-dialogue);
  background: var(--signal); color: var(--text-on-signal);
}
.mc-intro-btn:hover { background: var(--signal-hover); }
.mc-intro-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--signal-a55); }
/* the ROOM: light-navy back wall, floor gradient darker where it meets the wall */
.mc-intro-figure {
  flex: 0 0 36%; position: relative; min-height: 0;
  background: var(--navy-l2);
  border-left: var(--border-hairline) solid var(--white-a10);
}
.mc-intro-figure::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 22%; pointer-events: none;
  background: var(--grad-floor);
  box-shadow: inset 0 1px 0 var(--white-a06);
}
.mc-intro-figure img {
  position: absolute; bottom: 4%; left: 50%; transform: translateX(-50%);
  height: 88%; max-width: 92%; object-fit: contain; object-position: bottom;
  z-index: var(--z-l1); filter: drop-shadow(0 10px 22px var(--black-a35));
}
/* Guided Steps verdict mark (step.mark:"x", 2026-07-25): a red X snaps in over the
   step image and pulses twice, then holds. The overlay is keyed per step in the
   bundle, so re-entering the step replays it. Reduced motion: static X, no pulse. */
.gs-mark-x {
  position: absolute; inset: 0; z-index: var(--z-l2); pointer-events: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--incorrect-maroon-a14);
}
.gs-mark-x svg {
  width: min(38%, 190px); height: auto; overflow: visible;
  animation: gsMarkX var(--dur-t1900) var(--ease-out) both;
  filter: drop-shadow(0 4px 14px var(--black-a35));
}
.gs-mark-x path { stroke: var(--incorrect-vivid); stroke-width: 13; stroke-linecap: round; fill: none; }
@keyframes gsMarkX {
  0%   { opacity: 0; transform: scale(1.6); }
  18%  { opacity: 1; transform: scale(1); }
  38%  { transform: scale(1.14); }
  55%  { transform: scale(1); }
  72%  { transform: scale(1.14); }
  100% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .gs-mark-x svg { animation: none; } }

/* SPEECH MODE (2026-07-25): the narrator alone in her room — light-navy wall, floor
   gradient — with a comic-style bubble attached above her head (same configuration as
   the intro sequence's comic panel: white, 3px #0E1C42 border, centered tail pointing
   down at her) and one button beneath her. */
.mc-intro-modal--speech {
  /* AUTHOR'S LAYOUT, 2026-08-01 (his sketch): Loretta hard LEFT, full height, standing on the
     floor gradient; her speech in a wide rounded box TOP-RIGHT at head height; Continue below
     that box, centre-right, clear of the floor. Was a centred column with the bubble stacked
     above her. One change here reaches every intro AND outro on every template, because they
     all render the one IntroNarration component — that is the point of it being a template. */
  display: grid;
  grid-template-columns: minmax(150px, 30%) 1fr;
  grid-template-rows: auto 1fr;
  align-items: start;
  column-gap: clamp(16px, 3%, 34px); row-gap: var(--space-p18);
  /* THE BOTTOM PADDING RESERVES THE CONTINUE BUTTON'S ROW (author 2026-07-30). The button is
     gated on `gateContinue` — it only appears once she has finished speaking — and it used to be
     a normal flex child. In a `justify-content: flex-end` column, adding a child mid-scene
     pushes every earlier child UP, so Loretta jumped vertically the instant Continue faded in.
     Reserving its space here and taking the button out of flow (below) means her position is
     identical before and after, which also lets her stand higher than the modal's floor.
     --mc-intro-btn-row = 46px button (15px padding x2 + 16px line) + the 18px column gap. */
  --mc-intro-btn-row: 64px;
  /* Floor height, shared by the gradient and the button below — one number so they
     cannot drift apart. */
  --mc-intro-floor: 24%;
  /* Halved top/bottom air so she fills the stage (author 2026-08-01) — 30->15 above her hair,
     26->13 below her feet. The button's reserved row is untouched, so its late arrival still
     cannot move her. */
  padding: var(--space-p26) var(--space-p28) calc(13px + var(--mc-intro-btn-row));
  background: var(--navy-l2);
}
.mc-intro-modal--speech::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: var(--mc-intro-floor);
  pointer-events: none;
  background: var(--grad-floor);
  box-shadow: inset 0 1px 0 var(--white-a06);
}
/* PAUSE NARRATION (WCAG 2.2.2 / 2.1.2, author-placed 2026-09-06 — he marked the spot on a
   screenshot: "centered on the floor"). Dead centre of the FLOOR BAND, so it reads as part of
   this panel rather than page chrome, and clears everything already placed: Loretta holds the
   left ~30%, the speech box is top-right, and .mc-intro-btn sits above the floor at left:65%.
   Centring is done off --mc-intro-floor, the same variable the gradient uses, so the button
   and the band it sits in cannot drift apart if the floor is ever resized.
   z-index 2 to clear the ::after gradient. */
.mc-intro-pause {
  position: absolute; z-index: var(--z-l2);
  left: 50%; bottom: calc(var(--mc-intro-floor) / 2);
  transform: translate(-50%, 50%);
  width: var(--hit-target); height: var(--hit-target); padding: 0;          /* 44px: the touch-target size used course-wide */
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-circle); cursor: pointer;
  background: var(--white-a10);
  border: var(--border-w2) solid var(--white-a55);
  color: var(--text-on-navy);
}
.mc-intro-pause:hover { background: var(--white-a18); border-color: var(--white); }
.mc-intro-pause:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .mc-intro-pause { transition: none; } }
.mc-intro-speech-bubble {
  position: relative; z-index: var(--z-l1);
  grid-column: 2; grid-row: 1; align-self: start;
  width: 100%;
  background: var(--surface-card); color: var(--ink); border: var(--border-comic) solid var(--bar-deep); border-radius: var(--radius-r15);
  padding: var(--space-p13) var(--space-p18);
  font: var(--fw-bubble) var(--type-body-lg)/var(--lh-bubble) var(--font-ui);
  box-shadow: 0 6px 16px var(--ink-deep-a35); text-align: left;
}
/* Tail points LEFT, at her head — she now stands beside the box rather than under it. Two
   layers, outline then fill, so the wedge keeps the 3px dark border the body has. */
.mc-intro-speech-bubble::after {
  content: ""; position: absolute; right: 100%; top: var(--space-p34); margin-top: var(--space-n11);
  border: 11px solid transparent; border-right-color: var(--bar-deep);
}
.mc-intro-speech-bubble::before {
  content: ""; position: absolute; right: calc(100% - 4px); top: var(--space-p34); margin-top: var(--space-n8);
  border: 8px solid transparent; border-right-color: var(--surface-card); z-index: var(--z-l1);
}
.mc-intro-speech-figure {
  position: relative; z-index: var(--z-l1);
  /* spans both rows and sits on the floor: `end` alignment is what makes her STAND on the
     gradient rather than float above it, and the height lets her break well above the floor
     line as drawn. */
  grid-column: 1; grid-row: 1 / -1; align-self: end; justify-self: center;
  height: 96%; max-width: 100%; object-fit: contain; object-position: bottom;
  filter: drop-shadow(0 10px 22px var(--black-a35));
}
/* OUT OF FLOW, in the row reserved above — so its arrival cannot move the figure. Absolute
   rather than `visibility: hidden` on a placeholder, because the button is conditionally
   rendered in JS (intro-narration.js) and there is no element to reserve with when it is absent. */
.mc-intro-modal--speech .mc-intro-btn {
  /* Centred on the RIGHT COLUMN (the text side), not on the modal — Loretta occupies the left
     ~30%, so a modal-centred button sat visually under her rather than under her speech.
     Still absolute, still inside the reserved --mc-intro-btn-row, so its late arrival cannot
     move her: the gate means it appears only after she stops speaking. */
  margin: 0; z-index: var(--z-l2);
  position: absolute; left: 65%;   /* centre of the right column: 30% + 70%/2 */
  /* Clear of the floor by the SAME 26px the speech box gets against the modal edge
     (author 2026-08-01) — it used to sit at 26px from the modal bottom, which put it
     inside the gradient band rather than resting above it. Both distances now come
     from the one padding value, so restyling the modal keeps them equal. */
  bottom: calc(var(--mc-intro-floor) + 26px);
  transform: translateX(-50%);
}
@media (max-width: 1024px /* bp:tablet */) {
  /* the responsive player scrolls the document — pin the scrim to the viewport.
     Tablet keeps the side-by-side layout.
     INSET BY THE REAL CHROME (author 2026-07-27): the scrim used to cover the whole
     100vh viewport, so the flex-centered modal's own height cap (a flat 600px) was
     centered against space that included the fixed header/bottom bar — well short of
     the true visible window on a tall phone, leaving a chrome-navy gap between the
     modal and the bar. Insetting the scrim by --rc-header-h/--rc-footer-h (measured
     live, chrome-responsive.js) means "100%" here IS the true content window, so the
     modal can fill it properly instead of guessing a fixed pixel cap. */
  .mc-intro-scrim { position: fixed; top: var(--rc-header-h); bottom: var(--rc-footer-h); }
  /* no fixed px cap here (was min(600px,...) — 600 was STILL the binding constraint
     even after the scrim's own inset was fixed, since 600 < a typical phone's ~694px
     available height, so the gap barely moved on the first pass of this fix). The
     scrim above is already correctly bounded to the true content window, so 100% IS
     the right size — nothing left to cap it against. */
  .mc-intro-modal { width: 94%; height: 100%; }
  .mc-intro-panel { padding: var(--space-p26) var(--space-p24) var(--space-p20); }
  .mc-intro-text { font-size: var(--type-body-md); }
  /* SPEECH MODE ONLY, tablet/narrow (author 2026-08-01): the modal was stretched to the full
     scrim height, which left a large empty field above Loretta's head and floated her speech
     box away from her. It now SHRINKS TO ITS CONTENT and the scrim's existing
     `align-items: center` centres it vertically — no new centring mechanism needed.
     The air above her head is the modal's own top padding, set to 13px so it matches the
     padding inside the speech bubble beside her head, exactly as asked.
     Scoped to this breakpoint deliberately: desktop is a different composition and was not
     what the author was looking at. */
  .mc-intro-modal--speech {
    height: auto; max-height: 100%;
    padding-top: var(--space-p13);
  }
  /* `width: auto` is what makes the 13px true. With a stretched box, `object-fit: contain`
     letterboxes the spare space ABOVE her (object-position: bottom), so the gap above her hair
     was the letterbox, not the padding. Sizing by height and letting the width follow means the
     box hugs the artwork — and the art is alpha-cropped (CLAUDE.md §30.17), so the top of the
     image IS the top of her hair. */
  .mc-intro-modal--speech .mc-intro-speech-figure {
    height: clamp(240px, 44vh, 460px); width: auto; max-width: 100%;
  }
}
@media (max-width: 640px /* bp:phone-grid */) {
  /* phone: text ABOVE, the narrator below in her room */
  .mc-intro-modal { flex-direction: column; }
  .mc-intro-figure { flex: 0 0 32%; border-left: none; border-top: var(--border-hairline) solid var(--white-a10); }
  .mc-intro-figure img { height: 92%; }
  /* speech mode keeps its single column; just tighter type and a shorter figure */
  /* Same reserved button row, with this block's tighter 14px gap. */
  .mc-intro-modal--speech {
    flex-direction: column; gap: var(--space-p14);
    --mc-intro-btn-row: 60px;
    padding: var(--space-p20) var(--space-p16) calc(20px + var(--mc-intro-btn-row));
  }
  /* Phone: no room for a 30/70 split, so the halves stack — bubble on top, her beneath it,
     tail swung back to pointing DOWN at her head. The desktop layout is the author's spec;
     this is the same composition folded, not a different design. */
  .mc-intro-modal--speech { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  .mc-intro-speech-bubble { grid-column: 1; grid-row: 1; font-size: var(--type-body); padding: var(--space-p11) var(--space-p14); }
  .mc-intro-speech-bubble::after {
    right: auto; left: 50%; top: 100%; margin: var(--space-n11) 0 0 var(--space-n11);
    border-right-color: transparent; border-top-color: var(--bar-deep);
  }
  .mc-intro-speech-bubble::before {
    right: auto; left: 50%; top: calc(100% - 4px); margin: var(--space-n8) 0 0 var(--space-n8);
    border-right-color: transparent; border-top-color: var(--surface-card);
  }
  .mc-intro-speech-figure { grid-column: 1; grid-row: 2; height: 46%; }
  /* PHONE KEEPS THE BUTTON ON THE FLOOR GRADIENT — author-confirmed 2026-08-01, do not "fix"
     this to match tablet. Tablet/desktop rest the button ABOVE the gradient
     (`calc(var(--mc-intro-floor) + 26px)`); on phone the modal is a single stacked column and
     that placement would push the button off the bottom, so a flat 20px from the modal edge is
     correct here. The three breakpoints differ on purpose. */
  .mc-intro-modal--speech .mc-intro-btn { bottom: var(--space-p20); left: 50%; }
}
@media (prefers-reduced-motion: reduce) {
  .mc-intro-scrim { animation: none; }
  .mc-intro-item { transition: none; }
  .tpl-fb-card.is-leaving { animation: none; opacity: 0; }
  .mc-intro-scrim--outro .mc-intro-modal { animation: none; }
}
.mc {
  position: relative;                    /* anchors the floating Submit + feedback modal */
  display: flex; flex-direction: column;
  height: 100%; min-height: 0;
  background: var(--surface-chrome);
  border: var(--border-hairline) solid var(--white-a10);
  border-radius: var(--radius-card);
  overflow: hidden;
}
.mc-eyebrow {
  font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--signal); margin-bottom: var(--space-p8);
}

/* Upper: scenario rail + scene */
.mc-upper { display: flex; flex: 1 1 60%; min-height: 0; }
.mc-rail {
  /* Wider on desktop (author 2026-07-30: "I see no reason why the scenario information and what
     you observe couldn't take up more space on the left side"). This costs NOTHING vertically —
     the upper band is 1902px wide and was using only ~1224 of it (rail 360 + an aspect-locked
     scene), so ~680px sat empty. The scene is aspect-locked, so it cannot absorb spare width
     without also growing taller and stealing from the cards; the rail can, because its content
     is text that simply reflows. That is why this is the one free improvement here. */
  width: var(--mc-rail-w); flex-shrink: 0;
  background: var(--bar-deep);
  border-right: var(--border-hairline) solid var(--white-a10);
  padding: var(--space-p28) var(--space-p30); display: flex; flex-direction: column; gap: var(--space-p18); overflow: auto;
}
.mc-scenario-text { font: var(--fw-regular) var(--type-body-lg)/var(--lh-text) var(--font-ui); color: var(--text-on-navy); }
.mc-divider { height: var(--border-hairline); background: var(--on-navy-track); }
.mc-obs-list { display: flex; flex-direction: column; gap: var(--space-p12); }
.mc-obs { display: flex; align-items: center; gap: var(--space-p12); }
.mc-obs-icon { width: var(--size-p26); height: var(--size-p26); object-fit: contain; flex-shrink: 0; }
.mc-obs-marker {
  width: var(--size-p14); height: var(--size-p14); border-radius: var(--radius-circle); flex-shrink: 0;
  border: var(--border-w2-4) solid var(--signal);
}
.mc-obs-text { font: var(--fw-regular) var(--type-copy)/var(--lh-text-tight) var(--font-ui); color: var(--text-on-navy); }
.mc-scene-wrap { flex: 1; display: flex; align-items: center; justify-content: center; padding: 0 var(--space-p18); min-width: 0; }
.mc-scene {
  height: 96%; aspect-ratio: 21 / 9; max-width: 100%; flex-shrink: 0; position: relative;
  overflow: hidden; border-radius: var(--radius-option); background: var(--grey-6);
  box-shadow: inset 0 2px 22px var(--navy-a14); border: var(--border-hairline) solid var(--grey-10);
}
.mc-scene-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  /* Which breakpoint's focal applies is decided here, not in JS (same reason as --gs-focal-*:
     the component does not re-render on resize, so a JS breakpoint read would freeze at
     mount). choice-parts.js publishes all three; the img's object-position reads --mc-scene-focal. */
  --mc-scene-focal: var(--mc-scene-focal-desktop, center);
}
@media (max-width: 1024px /* bp:tablet */) {
  .mc-scene-img { --mc-scene-focal: var(--mc-scene-focal-tablet, var(--mc-scene-focal-desktop, center)); }
}
@media (max-width: 600px /* bp:phone */) {
  .mc-scene-img { --mc-scene-focal: var(--mc-scene-focal-phone, var(--mc-scene-focal-desktop, center)); }
}
.mc-thought {
  position: absolute; top: var(--space-p28); left: 50%; transform: translateX(-50%);
  background: var(--surface-card); border: var(--border-w1-5) solid var(--ink); border-radius: var(--radius-r22);
  padding: var(--space-p12) var(--space-p22); white-space: nowrap;
}
.mc-thought span { font: var(--fw-bold) var(--type-h4)/var(--lh-solid) var(--font-ui); color: var(--ink); }

/* Prompt bar */
.mc-prompt {
  /* `0 1 auto` + `min-width: 0`, NOT `0 0 auto` (author bug report 2026-08-07, tablet).
     A flex item defaults to min-width:auto, which is its CONTENT width — so with no shrink
     factor a long question simply refused to narrow: measured 899px inside a 753px row at
     768px wide, hanging 153px off the LEFT because the row centres it, and clipped at both
     ends. The text already wraps (`white-space: normal`); it was never allowed to get narrow
     enough to need to. max-width belts it so the pill can never exceed its row. */
  flex: 0 1 auto; min-width: 0; max-width: 100%;
  min-height: var(--size-p48); padding: var(--space-p8) var(--space-p24);
  display: flex; align-items: center; justify-content: center; background: var(--surface-chrome);
  font: var(--fw-bold) var(--type-h3)/var(--lh-snug) var(--font-ui); color: var(--text-on-navy); text-align: center;
  /* `margin: 0` since 2026-09-06: this is an <h2> (WCAG 1.3.1). A heading brings the browser's
     own margins where the div it replaced had none, and this is a FLEX ITEM in a centred row —
     an unreset margin-top would push the pill off the row's vertical centre at every width. */
  margin: 0;
}

/* Answer cards */
.mc-cards {
  /* `min-width: 0` is load-bearing at responsive widths (author bug report 2026-08-07:
     "the options are getting cut off on the right side, running off the page").
     This is a flex item of `.mc`, so its default min-width is AUTO — its own CONTENT
     width — and below 1024px it becomes a nowrap flex strip of cards. The strip therefore
     refused to shrink to its parent: measured 535px inside a 360px viewport, with 413px
     cards, so every option past the first was clipped by the ancestor's overflow. Every
     ancestor was correctly constrained to 360; this element alone was not.
     Same trap as `.mc-prompt` the same day. Overflow-x on the strip only does its job once
     the strip itself is allowed to be as narrow as the screen. */
  flex: 1 1 40%; min-width: 0; min-height: 0; background: var(--surface-chrome);
  /* The 78px bottom pad that used to reserve the floating Submit lane is gone (author
     2026-08-05): Submit now lives in the prompt row, so the cards keep this space. */
  display: grid; gap: var(--space-p20); padding: 0 var(--space-p48) var(--space-p16);
  grid-template-columns: repeat(var(--mc-cols, 4), 1fr);   /* --mc-cols set inline per page */
  /* CARDS SHARE ONE HEIGHT SO EVERY OPTION IMAGE STARTS AT THE SAME LINE (author-directed
     2026-08-07). This reverses the 2026-08-05 content-sizing, and the reason that one existed
     is now handled elsewhere: back then `stretch` dumped the surplus as dead white space UNDER
     the picture (43px against 11px at the sides), because `.mc-card-art` is `flex: 0 0 auto`
     and nothing above it could grow. `.mc-card-desc` is now `flex: 1 1 auto`, so the surplus
     goes into the TEXT block instead and the art still lands on the card's own bottom padding.
     Result: uniform card height, uniform bottom gap, and every picture at the same top edge —
     the author's words, "I would rather the buttons be uniform size altogether versus some of
     them being taller, some shorter." */
  align-items: stretch;
}
/* Desktop option carousel (author 2026-07-25): >5 options can't fit the one-row grid,
   so ChoiceCards renders the band as a scroll strip (scrollbar hidden) with arrow
   buttons at both ends; an arrow fades out at its end of the strip. ≤1024px the
   wrapper dissolves (display:contents) and the existing tablet/phone swipe carousels
   take over — touch pans, so the arrows are hidden there. */
.mc-cards-wrap { position: relative; flex: 1 1 40%; min-height: 0; display: flex; }
.mc-cards-wrap > .mc-cards { flex: 1 1 auto; min-width: 0; }
.mc-cards--scroll {
  /* `stretch`, not `flex-start` (author-directed 2026-08-07) — the strip has no shared row
     height, so content-sized cards left neighbours a few px apart and their pictures on
     different lines, which is exactly what the author objected to. Stretch was rejected on
     2026-08-05 for a reason that no longer applies: it dumped the surplus as dead space under
     the picture (41-55px against 11px at the sides on scenario-3) because nothing above the
     art could absorb it. `.mc-card-desc` is now `flex: 1 1 auto` and takes it instead, so the
     art still sits on the card's own bottom padding. */
  display: flex; flex-wrap: nowrap; align-items: stretch;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}
.mc-cards--scroll::-webkit-scrollbar { display: none; }
@media (min-width: 1025px /* bp:tablet +1 */) {
  /* 4 cards fully visible + a peek of the 5th (the peek is the scroll affordance) */
  .mc-cards--scroll .mc-card { flex: 0 0 calc((100% - 80px) / 4.35); scroll-snap-align: start; }
}
.mc-cards-arrow {
  position: absolute; z-index: var(--z-l5);
  top: calc((100% - 78px) / 2);            /* centered on the card area above the Submit zone */
  transform: translateY(-50%);
  width: var(--hit-target); height: var(--hit-target); border-radius: var(--radius-circle);
  border: var(--border-w1-5) solid var(--border); background: var(--surface-card-a94);
  color: var(--text-heading); font: var(--fw-bold) var(--type-h2)/var(--lh-solid) var(--font-ui);
  cursor: pointer; box-shadow: 0 4px 14px var(--black-a30);
  display: flex; align-items: center; justify-content: center;
  /* Optical centring of the chevron, MEASURED rather than eyeballed (author 2026-08-05:
     "it looks a little high"). The ink of `›` sits +0.5px below its own line-box centre, and
     a bottom padding of P lifts the flex-centred content by P/2 — so the offset from the
     button's centre is 0.5 − P/2. The old P=3 put it a full 1px HIGH; P=1 lands it at 0.00. */
  padding: 0 0 var(--space-p1);
  transition: opacity var(--dur-t160) var(--ease-default);
}
.mc-cards-arrow:hover { background: var(--surface-card); }
.mc-cards-arrow:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.mc-cards-arrow--left { left: var(--space-p6); }
.mc-cards-arrow--right { right: var(--space-p6); }
.mc-cards-arrow.is-off { opacity: 0; pointer-events: none; }
/* `is-end` = at that end of the strip, but the question is still unsolved, so the arrow
   stays VISIBLE instead of fading out (author 2026-07-28). Dimmed enough to read as
   "nothing further this way" while never leaving the learner with no visible control in
   either direction — which is what the old opacity:0 could do at the far end of a long
   option strip. Still interactive: `nudge` clamps, so the click is a no-op. */
.mc-cards-arrow.is-end { opacity: var(--alpha-42); }
.mc-cards-arrow.is-end:hover { opacity: var(--alpha-72); background: var(--surface-card); }
@media (max-width: 1024px /* bp:tablet */) {
  .mc-cards-wrap { display: contents; }
  .mc-cards-arrow { display: none; }
}
/* ---- Guided Multiple Choice: the stepped scene cycler (scene-cycler.js) -----------------
   Reuses `.mc-scene-wrap` / `.mc-scene` so the cycler occupies exactly the slot the single
   scene plate did, with the same aspect + focal handling. Only the contents are new. */
/* `.mc-scene-wrap` is a ROW flex container (it was only ever given one child), so a dots strip
   added as a sibling lands BESIDE the picture, vertically centred, overlapping it by ~122px.
   Scoped to the cycler's own class so the shared wrapper is untouched. */
.scy-wrap { flex-direction: column; justify-content: center; }
/* The base `.mc-scene` is 21/9 — right for a wide scene plate, wrong for step art drawn at
   1.25–1.41, which that ratio crops savagely. The cycler honours the screen's authored
   `scenario.scene.aspect` on DESKTOP too (the shared `.mc-scene--custom` rule only applies
   inside the narrow media query), falling back to 16/10 when none is authored. */
/* HEIGHT-DRIVEN at the AUTHORED ratio — the same sizing model as `.mc-scene`, just not locked
   to 21/9. Width-driven was wrong here (author 2026-08-05, with a marked-up screenshot): filling
   the column made the box 924px wide against source art only 762–939px across, so every step
   image was upscaled past its native resolution — visibly soft, on top of being cropped to a
   thin horizontal slice. Driving from height at 5/4 makes it TALLER and NARROWER, which frames
   the art properly and brings it back under 1:1 so it renders sharp.
   88% (not `.mc-scene`'s 96%) leaves the dots strip its row underneath. */
/* The picture and its two arrows, side by side. The arrows are ordinary flex items now, so they
   sit in the band's spare width instead of covering the art. */
.scy-row {
  display: flex; align-items: center; justify-content: center; gap: var(--space-p14);
  width: 100%; height: 85%; min-height: 0;
}
.scy {
  position: relative; overflow: hidden;
  height: 100%; width: auto; max-width: 100%; flex-shrink: 1; min-width: 0;
  aspect-ratio: var(--mc-scene-aspect, 15 / 8);
}
.scy-img { transition: opacity var(--dur-t200) var(--ease-default); }
/* The caption plate. A gradient, not a flat band: a hard edge across the middle of the art
   reads as a crop, while a gradient lets the picture carry through into the text. */
.scy-caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: var(--space-p34) var(--space-p20) var(--space-p16);
  background: var(--grad-caption-plate);
  color: var(--text-on-navy); text-align: left; pointer-events: none;
}
.scy-caption-title {
  font: var(--fw-extrabold) var(--type-copy)/var(--lh-snug) var(--font-head);
  color: var(--text-on-navy); margin-bottom: var(--space-p4);
}
.scy-caption-body {
  margin: 0; font: var(--fw-regular) var(--type-caption)/var(--lh-text-loose) var(--font-ui);
  color: var(--white-a94);
}
/* Arrows: same visual language as the option-strip arrows, and the same lesson learned —
   the centring transform is re-declared in :active so the press cannot slide the button out
   from under the pointer (see .mc-cards-arrow). */
.scy-arrow {
  flex: 0 0 auto; z-index: var(--z-l3);
  width: var(--size-p38); height: var(--size-p38); border-radius: var(--radius-circle);
  border: var(--border-w1-5) solid var(--border); background: var(--surface-card-a94);
  color: var(--text-heading); font: var(--fw-bold) var(--type-h4)/var(--lh-solid) var(--font-ui);
  display: flex; align-items: center; justify-content: center;
  padding: 0 0 var(--space-p1); cursor: pointer;
  box-shadow: 0 4px 14px var(--black-a30);
  transition: opacity var(--dur-t160) var(--ease-default);
}
.scy-arrow:hover:not(:disabled) { background: var(--surface-card); }
.scy-arrow:disabled { opacity: var(--alpha-32); cursor: default; }
/* No centring transform to preserve now that the arrow is a flex item, so the press is just
   the scale — nothing for it to replace. */
.scy-arrow:not(:disabled):active { transform: scale(0.94); }
.scy-arrow:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
/* Dots sit BELOW the picture, not on it — over the caption they would compete with the text,
   and under it they double as the progress read-out. `is-seen` is what makes the option gate
   legible: the learner can tell at a glance which images they have already stepped through. */
.scy-dots { display: flex; gap: var(--space-p8); justify-content: center; align-items: center; margin-top: var(--space-p10); }
.scy-dot {
  width: var(--size-p9); height: var(--size-p9); padding: 0; border-radius: var(--radius-circle); cursor: pointer;
  border: var(--border-w1-5) solid var(--white-a55); background: transparent;
  transition: background var(--dur-t160) var(--ease-default), border-color var(--dur-t160) var(--ease-default), transform var(--dur-t160) var(--ease-default);
}
.scy-dot.is-seen { border-color: var(--white-a85); background: var(--white-a45); }
.scy-dot.is-on { background: var(--signal); border-color: var(--signal); transform: scale(1.25); }
.scy-dot:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 3px; }
@media (max-width: 1024px /* bp:tablet */) {
  /* The narrow scene band is shorter, so the caption gets a tighter plate and the arrows
     shrink — same controls, never removed (they are the only way to advance on touch). */
  .scy-caption { padding: var(--space-p26) var(--space-p14) var(--space-p12); }
  .scy-caption-title { font-size: var(--type-small); }
  .scy-caption-body { font-size: var(--type-label); }
  .scy-arrow { width: var(--size-p34); height: var(--size-p34); font-size: var(--type-lead); }
}

.mc-card {
  position: relative; text-align: left; cursor: pointer; background: var(--surface-card);
  /* Bottom padding matched to the sides so the picture sits in an even frame (2026-08-05). */
  border: var(--border-w1-5) solid var(--border); border-radius: var(--radius-card); padding: var(--space-p14) var(--space-p16) var(--space-p16);
  display: flex; flex-direction: column; overflow: hidden;
  transition: border-color var(--dur-t140) var(--ease-default), box-shadow var(--dur-t140) var(--ease-default);
}
.mc-card.is-selected { border: var(--border-active) solid var(--signal); box-shadow: 0 0 0 4px var(--signal-a18); }
.mc-card.is-wrong { border: var(--border-active) solid var(--incorrect); box-shadow: 0 0 0 4px var(--incorrect-a15); opacity: var(--alpha-65); cursor: not-allowed; }
.mc-card:disabled:not(.is-wrong) { cursor: default; }
/* SUBMITTED, DEFERRED FEEDBACK (`immediateFeedback: false` — the graded quiz). Every option
   greys out the moment Submit is pressed, so the freeze is visible rather than merely true.
   The learner's own pick stays legible and keeps its blue selected border: it is the record of
   what they answered, and this screen deliberately says nothing about whether it was right. */
.mc.is-locked-out .mc-card { opacity: var(--alpha-45); filter: grayscale(0.85); transition: opacity var(--dur-t200) var(--ease-default), filter var(--dur-t200) var(--ease-default); }
.mc.is-locked-out .mc-card.is-selected { opacity: var(--alpha-90); filter: grayscale(0.25); }
.mc-card:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.mc-card-x {
  position: absolute; top: var(--space-p14); right: var(--space-p14); width: var(--size-p26); height: var(--size-p26); border-radius: var(--radius-circle);
  background: var(--incorrect); color: var(--text-on-accent); font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui);
  display: flex; align-items: center; justify-content: center;
}
.mc-card-head { display: flex; align-items: center; gap: var(--space-p12); margin-bottom: var(--space-p8); }
.mc-card-badge {
  width: var(--size-p32); height: var(--size-p32); flex-shrink: 0; border-radius: var(--radius-circle);
  background: var(--surface-chrome); color: var(--text-on-navy);
  font: var(--fw-extrabold) var(--type-body)/var(--lh-solid) var(--font-ui);
  display: flex; align-items: center; justify-content: center;
}
/* WCAG 1.4.3 (2026-09-06). Selected: white on --signal is 2.88:1, so the GLYPH goes navy
   (5.13:1) and the fill is untouched. Wrong: white on --incorrect is 3.98:1 and navy on it is
   3.72:1 - both fail - so the FILL darkens instead, scoped to this badge. --incorrect itself is
   deliberately NOT re-toned: it drives every error affordance in the course. */
.mc-card.is-selected .mc-card-badge { background: var(--signal); color: var(--text-on-signal); }
.mc-card.is-wrong .mc-card-badge { background: var(--incorrect-text); color: var(--text-on-navy); }
.mc-card-title { font: var(--fw-extrabold) var(--type-body-lg)/var(--lh-title) var(--font-ui); color: var(--ink); }
/* THE DESCRIPTION ABSORBS THE SLACK, SO EVERY OPTION IMAGE STARTS AT THE SAME HEIGHT
   (author-directed 2026-08-07: "keep the pictures the size they are... but try to keep the
   pictures at the same top height all the way across, like the top edge of all of them
   should be at the same area for each of them regardless of how many lines of text").
   The description is the ONLY variable-height part of a card — the badge/title row and the
   art box are both fixed — so a one-line option used to sit ~21px shorter than the two-line
   one beside it, and its picture rode 21px higher. `flex: 1 1 auto` here, with the cards
   stretched to a shared height (.mc-cards / .mc-cards--scroll), pushes the surplus INTO
   this block instead of leaving it under the picture. The art is `flex: 0 0 auto`, so it
   lands against the card's bottom padding in every card — same size, same top edge, same
   bottom gap. Cards with less text simply carry more space between text and picture.
   A FIXED TWO-LINE RESERVE WAS TRIED FIRST AND IS WRONG: measured across every choice page,
   five options already wrap past two lines (scenario-5-decision C, scenario-6-decision A/B,
   scenario-8-decision b at three; scenario-8-decision a at FOUR), so a two-line cap would
   have truncated real answer text. This approach has no cap and no line count to maintain —
   the tallest option on a page sets the height and the rest follow. */
.mc-card-desc {
  font: var(--fw-regular) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--secondary);
  margin-bottom: var(--space-p10);
  flex: 1 1 auto;
}
.mc-card-art {
  width: 100%; flex: 1; min-height: var(--size-p70); margin-top: var(--space-p8); position: relative;
  border-radius: var(--radius-r-10-10-0-0); overflow: hidden; background: var(--page);
}
/* WIDTH-DRIVEN, 4:3, AT EVERY BREAKPOINT (author 2026-07-29: "on mobile and tablet the images
   for the options are more square — the desktop ones should be too", "these are still tiny and
   they should be filling up the space for the options").
   Desktop used to size this art HEIGHT-first: choice-parts.js sets an inline `aspectRatio` with
   `width: auto; align-self: center`, so the box took whatever vertical slack the card's flex
   column had left after the title and body — a few dozen pixels — and the width followed it
   down. That is why the art read as a thumbnail centred in a wide card. Driving from the width
   makes the art fill the option, and the height follow the ratio, which is the same rule the
   narrow layouts already use. Per-page `cardArtAspect`/`cardArtMobileAspect` still override the
   shape through the var; only the SIZING AXIS is fixed here. */
.mc-card-art {
  /* DESKTOP: 4:3, sized from an explicit HEIGHT so the box is deterministic, then centred.
     Width-driven at full card width is what the author asked for, but it does not fit: at a
     589px card the 4:3 box is 416px tall inside a 280px card, so the art was clipped by the
     card's bottom edge. The fixed 1080 canvas cannot scroll, and making room would need a
     ~624px cards band, shrinking the scene image above to ~185px. So desktop gets the SHAPE
     (4:3, square-ish, per the author) at the largest height that fits, which is still ~4x the
     area of the old flex-slack thumbnail. `--mc-card-art-h` raises it per page if the layout
     is rebalanced. The narrow layouts keep the full-width rule below — they scroll, so they
     can afford the height. */
  /* 351x162 IS THE AUTHORED OPTION-IMAGE VIEWPORT (author 2026-07-29: "351 x 162 pixels is the
     size, a bunch of images were made specifically for that size... the viewport for all
     multiple choice questions for the images should be that size, and just crop and zoom in to
     fit any images that don't fit that size").
     So the ratio is the SYSTEM DEFAULT for every choice page, not a per-page value, and it is
     the same at every breakpoint — the art was generated for it. Scenario 3's seven plates are
     1846x852, which is exactly this ratio, confirming it. The box fills the card's width and
     takes its height from the ratio; anything whose native shape differs is cover-cropped into
     it rather than being shrunk to fit inside it. */
  aspect-ratio: var(--mc-card-art-aspect, 351 / 162) !important;
  width: 100% !important; height: auto !important;
  align-self: stretch !important; flex: 0 0 auto !important; min-height: 0 !important;
  max-width: var(--mc-card-art-max-w, none);
  margin-inline: auto;
}
.mc-card-art--img {
  /* `cover` rather than the inline 200% magnification: the frame now matches the art's shape,
     so filling it needs no zoom (the 200% default is what made these read "zoomed in"). */
  background-size: var(--mc-card-art-bg-size, cover) !important;
  background-position: var(--mc-card-art-bg-pos, center) !important;
}
.mc-card-art img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* Background-image variant (choice-parts.js): lets a wide scene be zoomed + panned to center
   its subject in the short card strip (background-size/position come inline, per option). */
.mc-card-art--img { background-repeat: no-repeat; background-color: var(--page); }

/* Wrong-answer card flip (author 2026-07-27, Multiple Choice): REPLACES the incorrect-
   feedback modal — the selected wrong card itself flips (same 3D mechanic as Flip Cards,
   .fc-card/.fc-inner/.fc-face above) to reveal a red INCORRECT header + feedback body + Try
   again, instead of covering the page with an overlay. The outer .mc-card-flip keeps the
   plain .mc-card box (border/background/radius/padding via the shared class) OFF — both
   faces draw their own chrome — and adds the 3D perspective; .mc-card-flip-inner is what
   actually rotates. First use: scenario-4-decision. */
.mc-card.mc-card-flip { position: relative; padding: 0; border: none; background: transparent; cursor: default; perspective: 1400px; }
.mc-card-flip-inner {
  position: absolute; inset: 0; transform-style: preserve-3d;
  transition: transform var(--dur-flip-delay) var(--ease-b-3-7-3-1);
}
.mc-card-flip.is-flipped .mc-card-flip-inner { transform: rotateY(180deg); }
.mc-card-flip-face {
  position: absolute; inset: 0; border-radius: var(--radius-card); overflow: hidden;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  display: flex; flex-direction: column;
}
/* front: the normal card look, red-bordered + dimmed the instant it's marked wrong —
   before the flip even starts, so the pause after Submit still reads as "that one". */
.mc-card-flip-front {
  background: var(--surface-card); border: var(--border-w2) solid var(--incorrect); opacity: var(--alpha-70);
  padding: var(--space-p14) var(--space-p16);
}
.mc-card-flip-front .mc-card-badge { background: var(--incorrect); }
/* back: white body under a solid red header — "Incorrect" is fixed chrome, not authored
   per-page, so the mechanic reads identically everywhere it's used. */
.mc-card-flip-back { transform: rotateY(180deg); background: var(--surface-card); border: var(--border-w2) solid var(--incorrect); }
.mc-card-flip-back-head {
  flex: 0 0 auto; background: var(--incorrect); color: var(--text-on-accent); text-align: center;
  padding: var(--space-p12) var(--space-p16); font: var(--fw-extrabold) var(--type-small)/var(--lh-title) var(--font-ui);
  letter-spacing: var(--track-hint); text-transform: uppercase;
}
.mc-card-flip-back-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--space-p14) var(--space-p16);
  font: var(--fw-regular) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--ink);
}
.mc-card-flip-tryagain {
  flex: 0 0 auto; margin: 0 var(--space-p16) var(--space-p14); padding: var(--space-p10); border-radius: var(--radius-pill); border: none;
  cursor: pointer; background: var(--signal); color: var(--bar-deep);
  font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui); transition: background var(--dur-t140) var(--ease-default);
}
.mc-card-flip-tryagain:hover { background: var(--play-hover); }
@media (prefers-reduced-motion: reduce) { .mc-card-flip-inner { transition: none; } }

/* ===== Shared feedback UI (feedback.js): floating Submit + correct/incorrect modal ==========
   Used by every scorable answer→submit→feedback template (Multiple Choice, Multi-Select,
   Scenario). Reproduced faithfully from the design tokens. The Submit and modal are absolutely
   positioned, so the host template's ROOT must be position:relative. */
.tpl-submit-wrap {
  position: absolute; left: 0; right: 0; bottom: var(--space-p14); z-index: var(--z-submit);
  display: flex; justify-content: center; pointer-events: none;
}
/* PROMPT ROW: the question centred, Submit parked over the LAST option column (author
   2026-08-05, "above where option C is … for all questions"). `right: 48px` is the same
   inset `.mc-cards` uses, so the button's right edge lines up with the last card's right
   edge at any column count — three options or seven, it never moves. The row is always in
   the DOM, so Submit fading in shifts nothing. Scoped to `.mc-prompt-row`, so scenario.js
   keeps the original bottom-centre float it was designed around. */
/* `width: 100%` is load-bearing: `.mc` is a column flex, so without it the row shrinks to the
   width of its own text (measured 224px) and `right: 48px` anchors to that instead of to the
   card band's edge. */
/* Padding is bottom-only and deliberately asymmetric: the scene band already leaves ~18px under
   the picture, so an even `8px 0` put 18px above the row and 8px below it. All of it on the
   bottom balances the row between the image and the options (author 2026-08-05: "equal padding
   between the image above and the options below"). */
.mc-prompt-row { position: relative; width: 100%; padding: 0 0 var(--space-p18); display: flex; align-items: center; justify-content: center; }
.mc-prompt-row .mc-prompt { margin-left: auto; margin-right: auto; }
/* Submit pins to the right edge of the option band — one predictable place on every question,
   whatever the option count. Column-alignment was built and then dropped (author 2026-08-05):
   with a varying number of options, "above option C" stops meaning anything. */
.mc-prompt-row .tpl-submit-wrap {
  left: auto; right: var(--space-p48); bottom: auto; top: 50%; transform: translateY(-50%);
  justify-content: flex-end; width: auto;
}
/* The lift on show is the row's, not the page's — `translateY(8px)` in the base rule would
   fight the centring transform above (the same replace-not-merge trap as the arrows). */
.mc-prompt-row .tpl-submit { transform: none; }
.tpl-submit {
  pointer-events: none; opacity: 0; transform: translateY(8px);
  font: var(--fw-extrabold) var(--type-copy)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-title);
  background: var(--signal); color: var(--bar-deep);
  border: none; border-radius: var(--radius-pill); padding: var(--space-p15) var(--space-p48); cursor: default;
  transition: opacity var(--dur-pop) var(--ease-standard), transform var(--dur-pop) var(--ease-standard), background var(--dur-t140) var(--ease-default);
}
.tpl-submit.is-shown { opacity: 1; transform: translateY(0); pointer-events: auto; cursor: pointer; }
.tpl-submit.is-shown:hover { background: var(--play-hover); }

/* WAITING, NOT MISSING — the choice templates show Submit greyed out before a selection
   (author-directed 2026-08-07). Background: once the prompt row stacks at ≤1024px, Submit
   became a full-width row of its own in the column flow, so at opacity 0 it left a 79px
   void between the question and the cards (measured: 47px button + 6px margin + 8px row gap
   + 18px row padding). The author read that gap as a rendering fault — "in order for it to
   not read like an error, we should make the submit button grayed out until the user selects
   an option." A visible disabled control explains its own space.
   SCOPED TO .mc-prompt-row on purpose. Every other host of FloatingSubmit (scenario.js and
   friends) floats it bottom-centre OVER the stage, where a permanently visible grey pill
   would be new furniture nobody asked for. Those keep the fade-in; only the choice
   templates, where the button has a reserved lane, show the waiting state.
   The button carries a real `disabled` (feedback.js), so this is appearance only — it is
   already out of the tab order and unclickable. */
.mc-prompt-row .tpl-submit:not(.is-shown) {
  opacity: 1; transform: none;
  background: var(--white-a13);
  color: var(--white-a42);
}

/* Feedback modal — a centered overlay over the STAGE only (player bars stay clear) */
.tpl-fb {
  position: absolute; inset: 0; z-index: var(--z-feedback); display: flex; align-items: center; justify-content: center;
  padding: var(--space-p32) var(--space-p64); background: var(--ink-deep-a80);
}
.tpl-fb-card {
  /* `relative` anchors the dev-only close X (feedback.js), which is absolutely positioned. */
  position: relative;
  width: 100%; max-width: var(--size-p1360); max-height: 100%;
  background: var(--surface-card); border-radius: var(--radius-bubble); overflow: auto;
  display: flex; flex-direction: column; box-shadow: 0 30px 80px var(--black-a50);
  animation: mvModalIn var(--dur-pop) var(--ease-overshoot);
}
.tpl-fb-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* EXPERIMENTAL narrator column (2026-07-25): Loretta beside the feedback text, no
   bubble, same room treatment as the MC intro modal. She also reads the feedback
   aloud (feedback.js). REVERT = stop passing `narrator` in the host templates and
   delete this block. */
.tpl-fb-card.has-narrator { flex-direction: row; overflow: hidden; }
.tpl-fb-card.has-narrator .tpl-fb-main { overflow: auto; }
.tpl-fb-figure {
  flex: 0 0 25%; position: relative; min-height: 0;
  background: var(--navy-l2); border-left: var(--border-hairline) solid var(--navy-d11-a18);
}
.tpl-fb-figure::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 22%;
  pointer-events: none;
  background: var(--grad-floor);
  box-shadow: inset 0 1px 0 var(--white-a06);
}
.tpl-fb-figure img {
  position: absolute; bottom: 4%; left: 50%; transform: translateX(-50%);
  height: 88%; max-width: 90%; object-fit: contain; object-position: bottom;
  z-index: var(--z-l1); filter: drop-shadow(0 10px 22px var(--black-a35));
}
/* DESKTOP: LANDSCAPE MODAL — Loretta stands FULL HEIGHT on the LEFT, her words on the right
   (author 2026-07-31). Two earlier attempts are recorded here because both were wrong in the
   same direction: a `cover` crop scaled her to the column's WIDTH and showed only her head, and
   a hips-up crop still read as a floating bust. She is a standing character; she should stand.

   The card gets a min-height so there is a room for her to stand in — without it the card is
   only as tall as its text, and a full-height figure inside a short card is small again, which
   was the original complaint. The floor gradient comes back, because there are feet to ground.
   Tablet and phone are deliberately untouched below. */
@media (min-width: 1025px /* bp:tablet +1 */) {
  /* SAME HEIGHT AS THE INTRO MODAL (author 2026-08-01): .mc-intro-modal is
     `height: min(620px, 90%)`, and the feedback card read as noticeably thinner beside
     it. min-height rather than height so a long body can still grow the card instead of
     scrolling inside a fixed box. */
  .tpl-fb-card.has-narrator { min-height: min(620px, 90%); }
  .tpl-fb-figure {
    order: -1; flex: 0 0 30%;
    border-left: none; border-right: var(--border-hairline) solid var(--navy-d11-a18);
  }
  .tpl-fb-figure img {
    position: absolute; bottom: 4%; left: 50%; right: auto; top: auto;
    transform: translateX(-50%);
    /* Taller so she fills the space, NOT stretched: `contain` + `width: auto` means the
       browser scales her by aspect ratio and the height is the only driver. Setting both
       width and height is what would distort her. */
    height: 97%; width: auto; max-width: 96%;
    object-fit: contain; object-position: bottom;
    filter: drop-shadow(0 10px 22px var(--black-a35));
  }
}

/* Narrow layouts keep the narrator (author 2026-07-27; previously dropped): */
@media (max-width: 1024px /* bp:tablet */) {
  /* tablet: Loretta holds the RIGHT THIRD of the modal */
  .tpl-fb-figure { flex: 0 0 33%; }
}
@media (max-width: 600px /* bp:phone */) {
  /* phone: Loretta STACKS ABOVE the feedback and the whole card scrolls, so the
     text is read by scrolling down past her */
  .tpl-fb-card.has-narrator { flex-direction: column; overflow: auto; }
  .tpl-fb-card.has-narrator .tpl-fb-main { overflow: visible; }
  .tpl-fb-figure {
    order: -1; flex: 0 0 auto; height: var(--size-p220);
    border-left: none; border-bottom: var(--border-hairline) solid var(--navy-d11-a18);
  }
}
/* Outro hand-off: the card alone fades out while the dark overlay holds; the outro
   speech modal then enters card-only (its scrim skips the re-fade below). */
.tpl-fb-card.is-leaving { animation: mvFbCardOut var(--dur-t250) var(--ease-default) forwards; }
@keyframes mvFbCardOut { to { opacity: 0; transform: translateY(10px) scale(0.985); } }
.mc-intro-scrim--outro { animation: none; }
.mc-intro-scrim--outro .mc-intro-modal { animation: mvModalIn var(--dur-t300) var(--ease-standard); }
.tpl-fb-head { display: flex; align-items: center; gap: var(--space-p16); padding: var(--space-p28) var(--space-p44); }
.tpl-fb-head.is-correct { background: var(--correct); }
.tpl-fb-head.is-wrong { background: var(--incorrect); }
.tpl-fb-mark {
  width: var(--size-p40); height: var(--size-p40); flex-shrink: 0; border-radius: var(--radius-circle); background: var(--surface-card);
  font: var(--fw-extrabold) var(--type-h5)/var(--lh-solid) var(--font-ui);
  display: flex; align-items: center; justify-content: center;
}
.tpl-fb-head.is-correct .tpl-fb-mark { color: var(--correct); }
.tpl-fb-head.is-wrong .tpl-fb-mark { color: var(--incorrect); }
.tpl-fb-title { font: var(--fw-extrabold) var(--type-h2)/var(--lh-title) var(--font-ui); color: var(--text-on-navy); text-transform: uppercase; letter-spacing: var(--track-title); }
.tpl-fb-body { flex: 1; display: flex; flex-direction: column; padding: var(--space-p40) var(--space-p44); }
.tpl-fb-text { position: relative; font: var(--fw-regular) var(--type-lead-lg)/var(--lh-body) var(--font-ui); color: var(--ink); margin: 0 0 var(--space-p26); }
.tpl-fb-takeaway {
  background: var(--signal-a06); border: var(--border-hairline) solid var(--signal-a25); border-radius: var(--radius-option);
  padding: var(--space-p20) var(--space-p24); margin-bottom: var(--space-p34);
  font: var(--fw-regular) var(--type-body-lg)/var(--lh-prose) var(--font-ui); color: var(--ink);
}
.tpl-fb-takeaway b { color: var(--signal); font-weight: var(--fw-extrabold); }
/* Type-on size reservation (author 2026-07-27, "the modal shouldn't grow as she talks") —
   .tpl-fb-ghost renders the FULL final text `visibility:hidden` so it still occupies layout
   space; .tpl-fb-live is the real animated reveal, absolutely positioned on top of it (same
   font/width as the ghost, so it wraps identically). .tpl-fb-reserve is only needed where the
   ghost's container has its own padding (.tpl-fb-takeaway) — inset:0 positions against the
   padding box, so without an unpadded wrapper the live layer would ignore that padding.
   .tpl-fb-text has no padding of its own, so it can be the position:relative anchor directly. */
.tpl-fb-reserve { position: relative; }
.tpl-fb-ghost { visibility: hidden; }
.tpl-fb-live { position: absolute; inset: 0; }
/* CENTRED in the white panel (author 2026-08-01). Was flex-end, which put it in the far
   corner; the card now reads as one composition with the figure, so the action belongs
   centred under the words it follows. */
/* PAUSE NARRATION on the feedback card (WCAG 2.2.2 / 2.1.2, author-placed 2026-09-06:
   bottom right, inside the modal). Anchored to .tpl-fb-main - the WHITE card - rather than the
   whole modal, so it stays tied to the text being spoken instead of floating over the narrator
   panel. .tpl-fb-actions is centred, so this can never collide with Continue no matter how long
   the feedback runs; and the verdict bar is the loud element here, so this is deliberately
   muted rather than signal-blue.
   .tpl-fb-main is the positioning context (position: relative, set below). */
.tpl-fb-main { position: relative; }
.tpl-fb-pause {
  position: absolute; right: var(--space-p18); bottom: var(--space-p16); z-index: var(--z-l2);
  width: var(--size-p40); height: var(--size-p40); padding: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-circle); cursor: pointer;
  background: var(--grey-3); border: var(--border-w2) solid var(--muted); color: var(--text-heading);
}
.tpl-fb-pause:hover { background: var(--grey-7); border-color: var(--text-heading); }
.tpl-fb-pause:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 3px; }
.tpl-fb-actions { display: flex; justify-content: center; margin-top: auto; }
.tpl-fb-btn {
  font: var(--fw-extrabold) var(--type-copy)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-title);
  background: var(--signal); color: var(--bar-deep);
  border: none; border-radius: var(--radius-pill); padding: var(--space-p15) var(--space-p44); cursor: pointer;
  transition: background var(--dur-t140) var(--ease-default);
}
.tpl-fb-btn:hover { background: var(--play-hover); }

@media (prefers-reduced-motion: reduce) {
  .tpl-submit { transition: opacity var(--dur-t0); }
  .tpl-fb-card { animation: none; }
}

/* RESPONSIVE reflow (matches the other template reflows: 1024 → tablet, 600 → phone). The fixed
   two-region desktop composition (360px rail + scene beside 4-up cards) does not fit a narrow
   screen, so: the page grows, the rail stacks above the scene, the prompt shrinks, the Submit
   sticks to the bottom of the scroll area, and the feedback modal covers the viewport.
   OPTIONS ARE A HORIZONTAL SWIPE CAROUSEL on every touch size — all N choices in ONE row, so
   there is no second grid row to scroll down to (which would push the scenario + observations
   out of view). Tablet shows ~2 cards at once; phone shows ~1 (the 600px block below). The scene
   is capped to a banner so the scenario text + observations stay visible above the carousel. */
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--multiple-choice { padding: 0; }
  .mc { height: auto; min-height: 100%; border-radius: 0; border: none; }
  .mc-upper { flex-direction: column; flex: 0 0 auto; }
  .mc-rail { width: auto; gap: var(--space-p14); padding: var(--space-p20) var(--space-p18); }
  .mc-scene-wrap { padding: var(--space-p16); }
  /* THE SCENE SHOWS WHOLE FROM 1024px DOWN (author 2026-07-31). It was a fixed 200px banner
     here and 104px on phone, both cover-cropped — which cut the heads off the figures anywhere
     between ~650px and 1024px. There is no reason to cap the height: the plate already fits
     horizontally, the page scrolls at these widths, and a cropped scene is worse than a tall
     one. Width is untouched; only the height cap is lifted, so the box takes the image's own
     aspect. `aspect-ratio: auto` is required because the base .mc-scene rule pins 21/9, and
     `--custom` is included because a per-screen aspect is itself a crop.
     DESKTOP (>1024px) IS DELIBERATELY UNTOUCHED — it is a fixed canvas that cannot scroll. */
  .mc-scene { width: 100%; }
  /* Supersedes the 2026-07-27 per-screen `--mc-scene-aspect` opt-in: that existed so a
     square/portrait plate could escape the banner crop, and now NOTHING is cropped here, so
     every scene gets what that override was asking for. The property is still read on desktop. */
  .mc-scene,
  .mc-scene.mc-scene--custom { height: auto; aspect-ratio: auto; }
  .mc-scene-img { position: static; width: 100%; height: auto; object-fit: contain; }
  .mc-prompt { font-size: var(--type-lead); padding: var(--space-p12) var(--space-p16); }
  .mc-cards {
    display: flex; flex-wrap: nowrap;
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x mandatory; scroll-padding-inline: 16px;
    -webkit-overflow-scrolling: touch;
    grid-template-columns: none; grid-auto-rows: auto;
    gap: var(--space-p14); padding: var(--space-p16); scrollbar-width: none;
    /* `cardsMaxWidth` IS A DESKTOP LEVER AND MUST BE NEUTRALISED HERE (author bug report
       2026-08-07: "the options are getting cut off on the right side, running off the page").
       choice-parts.js implements that knob as `max-width` PLUS `margin-inline: auto` to centre
       the band on the 1920 stage. The auto margin is the problem: on the cross axis it
       DISABLES flex stretch, so this band stopped filling its parent and shrink-wrapped to its
       own content — measured 535px inside a 360px viewport, clipping every option past the
       first. Proven by bisection: clearing margin-inline alone restores 360px; clearing
       max-width alone changes nothing.
       !important because the renderer writes both as INLINE styles.
       Affects the five screens that set the knob — scenario-1/-4/-6-decision had it before
       today, scenario-5 and -8 were given it on 2026-08-07. Capping card width is meaningless
       here anyway: below 1024px the band is a scroll strip and card width comes from the
       `flex: 0 0 44%` rule below. */
    margin-inline: 0 !important;
    max-width: none !important;
  }
  .mc-cards::-webkit-scrollbar { display: none; }
  .mc-card { flex: 0 0 44%; scroll-snap-align: start; min-height: var(--size-p220); }   /* ~2 visible + a peek */
  /* Card art on TABLET+PHONE is WIDTH-driven, 4:3 by default (author 2026-07-29: "I just want
     them to be as large as possible within the width of each of those options", "4:3 for all
     of these").
     Desktop's fixed-ratio art is sized HEIGHT-first (from the flex row's own height, via
     `flex:1`) with `width:auto` + `align-self:center` following the ratio — that's backwards
     here: a mobile card's height is mostly text, so the leftover flex slack is tiny, and a
     box sized from THAT collapses to a tiny box with big blank margins either side (author
     bug reports 2026-07-27 "why are the images so tiny" and 2026-07-29, twice). Flip the
     relationship: stretch to the card's full WIDTH and let height follow the ratio.
     THIS USED TO BE OPT-IN, and that was the bug. The rule only applied to
     `.mc-card-art--mobile-aspect`, a class the renderer adds solely when the page authored
     `cardArtMobileAspect`/`imageZoomMobile`/`imagePosMobile` — so scenario-1-decision,
     scenario-3-verify and scenario-4-decision, which author none of those, silently kept the
     height-driven desktop sizing and collapsed. A default that only works when a page opts
     in is a default that will be missing on the next page too, so the width-driven form is
     now the plain behaviour for every card and the per-page vars remain as overrides.
     !important throughout: the base ratio/size are INLINE styles on this element, which beat
     any plain class rule regardless of specificity.
     `cover` (not the inline 200% zoom) is the default fill: the art fills a frame it now
     actually matches in shape, instead of being magnified 2x inside a collapsed one. */
  /* The narrow layouts inherit the width-driven 4:3 base rule above; these only swap in the
     per-page MOBILE overrides, which must beat the desktop-scoped vars. */
  .mc-card-art {
    aspect-ratio: var(--mc-card-art-mobile-aspect, var(--mc-card-art-aspect, 351 / 162)) !important;
    width: 100% !important; height: auto !important;
    align-self: stretch !important; max-width: none !important;
  }
  .mc-card-art--img {
    background-size: var(--mc-card-art-mobile-bg-size, var(--mc-card-art-bg-size, cover)) !important;
    background-position: var(--mc-card-art-mobile-bg-pos, var(--mc-card-art-bg-pos, center)) !important;
  }
  /* Shared feedback UI responsive (applies to every scorable template, not just MC): Submit
     rides the bottom of the scroll viewport; the modal covers the viewport (stage is now tall). */
  .tpl-submit-wrap { position: sticky; bottom: var(--space-p12); margin-top: var(--space-p6); }
  .tpl-fb { position: fixed; padding: var(--space-p16); }
  .tpl-fb-card { max-height: calc(100dvh - 32px); }
  .tpl-fb-head { padding: var(--space-p20) var(--space-p24); gap: var(--space-p12); }
  .tpl-fb-mark { width: var(--size-p32); height: var(--size-p32); font-size: var(--type-copy); }
  .tpl-fb-title { font-size: var(--type-lead); }
  .tpl-fb-body { padding: var(--space-p24); }
  .tpl-fb-text { font-size: var(--type-body-md); margin-bottom: var(--space-p20); }
  .tpl-fb-takeaway { font-size: var(--type-body); padding: var(--space-p16) var(--space-p18); margin-bottom: var(--space-p24); }
  .tpl-fb-btn { padding: var(--space-p14) var(--space-p32); }
}
@media (max-width: 600px /* bp:phone */) {
  /* PHONE: same horizontal carousel as tablet (defined in the 1024px block), but ONE card at a
     time (~82% wide, so the next peeks) and the scenario compacted harder — smaller scene banner,
     thought bubble hidden — so the observations still fit above the options.
     BREAKPOINT: 600px is the conventional phone/tablet line (Material sw600dp) and also the
     practical floor for a 2-up view (two readable cards need ~280px each). ≤600px = ~1 card;
     601–1024px = ~2 cards (the block above); >1024px = the 4-up desktop composition. */
  .mc-card { flex: 0 0 82%; scroll-snap-align: center; }   /* one at a time; next card peeks */
  .mc-cards { gap: var(--space-p12); padding: var(--space-p12) var(--space-p16) var(--space-p16); }
  /* Compact the scenario so it (text + observations) stays in view above the carousel. */
  .mc-rail { padding: var(--space-p14) var(--space-p16); gap: var(--space-p10); }
  .mc-obs-list { gap: var(--space-p8); }
  .mc-scenario-text { font-size: var(--type-body); }
  .mc-obs-text { font-size: var(--type-small); }
  .mc-scene-wrap { padding: var(--space-p8) var(--space-p16); }
  /* The uncropped scene is inherited from the 1024px block above — it applies from there all
     the way down, so phone needs no rule of its own beyond this tighter gutter. */
  .mc-thought { display: none; }               /* the prompt bar already asks the question */
  .mc-prompt { font-size: var(--type-copy); padding: var(--space-p8) var(--space-p14); }
}

/* ===== Multi-Select — "select all that apply" =====================================
   Now the SAME scenario-question layout as Multiple Choice (choice-parts.js / the shared .mc-*
   classes), with MULTIPLE selectable cards. Only the multi-select extras live here: the card
   checkbox marker and the instruction sub-line. Page padding mirrors Multiple Choice. */
.tpl-page--multi-select { padding: var(--space-p8); }

/* checkbox marker on each card (top-right) — the select-all affordance */
.mc-card-check {
  position: absolute; top: var(--space-p14); right: var(--space-p14); width: var(--size-p24); height: var(--size-p24); flex-shrink: 0;
  border-radius: var(--radius-badge); border: var(--border-w2) solid var(--disabled-btn); background: var(--surface-card);
  display: flex; align-items: center; justify-content: center;
  font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui); color: var(--text-on-navy);
}
.mc-card.is-selected .mc-card-check { background: var(--signal); border-color: var(--signal); }
/* keep the card title clear of the checkbox */
.tpl-page--multi-select .mc-card-head { padding-right: var(--space-p30); }

/* the "select all that apply" instruction — inline on the prompt line (lighter + smaller) */
.mc-prompt-inst { color: var(--white-a62); font-weight: var(--fw-medium); font-size: var(--type-lead); margin-left: var(--space-p12); }

@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--multi-select { padding: 0; }
  .mc-prompt-inst { font-size: var(--type-small); margin-left: var(--space-p8); }
}

/* THE PROMPT ROW STACKS BELOW 1024px (author bug report 2026-08-07, phone + tablet).
   On desktop the row is one line — question pill centred, Submit parked in a 160px lane at
   the right — and that is right there, because the stage is 1920 wide. Narrow, the same
   layout turns into three cramped columns: the question squeezed into the left half, "Select
   all that apply." wrapping one or two words per line beside it, and the Submit lane holding
   the right quarter empty until an answer is picked. The author's words: "they should be
   vertically stacked... it seems to avoid the right half of the screen on that row for no
   reason."
   Column direction gives the question the full width, drops the instruction onto its own
   line, and moves Submit underneath instead of alongside. Submit KEEPS `position: sticky`
   (set at 1024px) — it is now a full-width row of its own, so sticking to the bottom still
   works and nothing horizontal is reserved. */
@media (max-width: 1024px /* bp:tablet */) {
  .mc-prompt-row { flex-direction: column; align-items: stretch; gap: var(--space-p8); }
  .mc-prompt-row .mc-prompt { width: 100%; max-width: 100%; margin: 0; }
  .mc-prompt-row .tpl-submit-wrap {
    top: auto; right: auto; left: auto; transform: none;
    width: 100%; justify-content: center;
  }
  /* The instruction is a <span> inside .mc-prompt, and .mc-prompt is itself a FLEX ROW — so
     `display: block` on the span does nothing at all (a flex item's display is blockified and
     it stays on the main axis; measured, it sat at x=300 of a 375px screen, still beside the
     question). Turning the pill's own axis is what actually stacks them: the question is an
     anonymous flex item, the span is the second, and column puts one under the other. */
  .mc-prompt { flex-direction: column; }
  .mc-prompt-inst { margin-left: 0; margin-top: var(--space-p4); text-align: center; }
}

/* ===== Welcome — course landing / title screen ====================================
   Demo-only template reproduced faithfully from the design. The background is ONE inline
   <svg> (viewBox 1672×941) holding the raster desk plate AND the animated steam in a single
   coordinate system, so any cover-crop / scale applies to BOTH and the steam can't drift off
   the mug (renderers/templates/welcome.js). Two plates live in the SVG and swap by breakpoint:
   the DESKTOP plate bakes the title onto the phone screen (no visible card); the MOBILE plate
   is blank and the readable title card STACKS beneath. The card stays in the DOM either way —
   visible on tablet/phone, screen-reader-only on desktop — so the baked title is still
   announced. The hero is ASPECT-LOCKED to the image on desktop (matching aspect ⇒ the SVG
   fills exactly, no crop). Not interactive, not scored. */
.tpl-page--welcome {
  padding: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-stage);
}
.wel {
  position: relative; container-type: inline-size;
  height: 100%; aspect-ratio: 1672 / 941; max-width: 100%;
}
.wel-hero { position: absolute; inset: 0; min-width: 0; overflow: hidden; }
/* The SVG fills the hero (position:absolute; inset:0; 100%×100%) and covers via its own
   preserveAspectRatio="xMidYMid slice" — native, robust cover, no CSS sizing trick. Desktop: hero
   aspect == image aspect, so slice fills exactly (no crop). Tablet: the banner is shorter, so slice
   cover-crops CENTERED — and the whole mug still shows, so tablet is left as-is. The steam lives in
   the same viewBox, so the crop applies to it too and it stays glued to the mug. */
.wel-svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* PHONE ONLY (<=600px → .rc-shell.is-phone, the player's single layout authority): as the aspect
   narrows, a CENTERED slice starts to clip the mug at the right edge. So on phones bias the crop
   toward the phone-screen ↔ mug boundary (~61% across) to keep the coffee mug — and its steam — in
   focus. object-fit / object-position don't apply to an inline SVG, so reproduce
   object-position: 61% 50% deterministically: oversize the SVG past the hero (min-width/height +
   aspect-ratio give the cover size) and anchor its 61%/50% point to the hero's 61%/50% point via
   translate. Tablet + desktop are deliberately NOT affected (base rule above). */
.rc-shell.is-phone .wel-svg {
  --wel-focus-x: 61%;   /* the phone-screen/mug boundary — tune here */
  inset: auto; top: 50%; left: var(--wel-focus-x); translate: calc(-1 * var(--wel-focus-x)) -50%;
  width: auto; height: auto; min-width: 100%; min-height: 100%; aspect-ratio: 1672 / 941;
}
.wel-plate { display: block; }
.wel-plate--mobile { display: none; }   /* desktop shows the baked-title plate */

/* Steam — thin, blurred, low-opacity wisps rising from the mug opening (~x 1150, y 505 in the
   1672×941 canvas), layered over the plate's own baked plume for a touch of life. Transform in
   user units (transform-box: fill-box). Pure CSS; reduced-motion disables it (baked plume only). */
.wel-steam { pointer-events: none; }
.wel-wisp {
  fill: none; stroke: var(--white); stroke-width: 11; stroke-linecap: round;
  filter: blur(4px); opacity: 0;
  transform-box: fill-box; transform-origin: 50% 100%;
  animation: wel-wisp-rise var(--dur-t6000) var(--ease-in-out) infinite;
}
.wel-wisp--a { --wisp-peak: 0.22; animation-duration: var(--dur-t5600); }
.wel-wisp--b { --wisp-peak: 0.16; animation-duration: var(--dur-t7200); animation-delay: calc(-1 * var(--dur-t2800)); }
@keyframes wel-wisp-rise {
  0%   { opacity: 0;                                transform: translate(0, 5px)    scaleY(0.96); }
  22%  { opacity: var(--wisp-peak, 0.2); }
  55%  {                                            transform: translate(-6px, -12px) scaleY(1.02); }
  80%  { opacity: calc(var(--wisp-peak, 0.2) * 0.45); }
  100% { opacity: 0;                                transform: translate(4px, -28px)  scaleY(1.06); }
}
@media (prefers-reduced-motion: reduce) {
  .wel-wisp { animation: none; opacity: 0; }   /* leave only the plate's baked plume */
}

/* DEV MODE (testing only) — player.js flags html.dev-mode when the author enables the menu's dev pill.
   A PERPETUAL (infinite) animation — steam, twinkles, pulses, the blinking carets — keeps the compositor
   busy forever, so an automated screenshot's render-idle wait never settles. Freeze ONLY those so a still
   frame can be captured. We must NOT freeze one-shot reveals (options fading in, heads rising, speech
   bubbles appearing): they settle on their own, and pausing them at frame 0 is what left the comic
   conversation looking empty under dev mode. So dev mode now behaves like normal play — the reveals run —
   except the infinite loops hold still. Add any NEW `infinite` animation's selector to this list.
   REMOVE BEFORE SHIPPING (with the dev pill). */
html.dev-mode .ppt-comic-caret,
html.dev-mode .cvo-choice .dlg-shell,
html.dev-mode .wel-wisp,
html.dev-mode .scn-obs-dot,
html.dev-mode .scn-hot-pulse,
html.dev-mode .scn-hot-dot,
html.dev-mode .obs-caret {
  animation-play-state: paused !important;
}

/* Desktop: the title is baked into the plate, so the card is visually hidden — but kept in the
   DOM (screen-reader only) so the eyebrow/title/subtitle are still announced. */
.wel-card {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.wel-eyebrow {
  letter-spacing: var(--track-wel-eyebrow); text-transform: uppercase; color: var(--signal);
  font-family: var(--font-ui); font-weight: var(--fw-extrabold);
}
.wel-title {
  font-family: var(--font-display); font-weight: var(--fw-black);
  color: var(--text-on-navy); letter-spacing: var(--track-wel-title);
}
.wel-subtitle {
  font-family: var(--font-ui); font-weight: var(--fw-medium);
  color: var(--white-a90);
}

/* Tablet + phone: stack (image banner + readable card below). The blank MOBILE plate shows in
   the banner and the card becomes a normal panel with the title text. */
@media (max-width: 1024px /* bp:tablet */) {
  /* CAUSE OF THE NAVY BAND: the shell reserves .rc-content { padding-bottom: 96px } (player.css)
     for the controls, but the real .rc-bottom bar is shorter (~65px), so ~31px of that padding
     showed as empty navy below the card. For the WELCOME screen ONLY, drop the reservation so the
     content area runs to the viewport bottom (behind the fixed bar); the bar still sits on top. */
  .rc-content:has(.tpl-page--welcome) { padding-bottom: 0; }
  .tpl-page--welcome {
    /* The shared .tpl-page base rule sets flex:1 (flex-basis:0%) — inside a flex
       COLUMN, a 0% flex-basis overrides the element's own `height` property
       entirely (spec behavior), so the `height` below was silently dead: this
       page's actual height came from auto-sizing to its content instead, and
       since .wel-hero claims a FIXED 57vh regardless of what's available,
       .wel-card was left with whatever scrap remained — 38px on a typical
       phone, clipping almost all of the eyebrow/title/subtitle (author bug
       report, 2026-07-27). flex-basis:auto here makes `height` the used basis
       again, scoped to this one page — every other template's default is
       untouched. */
    display: flex; flex-direction: column; align-items: stretch; overflow: hidden;
    flex-basis: auto;
    height: calc(100vh - var(--rc-header-h, var(--chrome-flow-header-h)));
    background: var(--bar-deep);
  }
  /* align-items:stretch (above) is load-bearing: the base rule centers .wel, which in a flex
     COLUMN would shrink-wrap it to min-content (~40px) and crush the hero + card into a strip.
     width:100% + min-width:0 make .wel own the viewport width — the container sizes the art, not
     the art (its aspect-ratio) sizing the container. */
  .wel { flex: 1; min-height: 0; width: 100%; min-width: 0; aspect-ratio: auto; max-width: none; container-type: normal; display: flex; flex-direction: column; }
  /* FIXED hero height so dropping the padding grows ONLY the card, never the image. The SVG
     inside cover-crops itself via preserveAspectRatio="slice" (base rule) — no per-breakpoint
     SVG sizing needed. */
  .wel-hero { position: relative; inset: auto; flex: 0 0 auto; height: 57vh; min-height: 0; width: 100%; min-width: 0; }
  .wel-plate--desktop { display: none; }
  .wel-plate--mobile { display: block; }
  .wel-card {
    position: static; width: auto; height: auto; margin: 0; clip: auto; clip-path: none;
    white-space: normal; overflow: hidden;
    border: none; border-top: var(--border-w3) solid var(--signal); border-radius: 0;
    background: var(--bar-deep); box-shadow: none;
    /* left/right = the invisible border the text lives in (scales a little with width); top + gap
       are ~one small-text line; bottom clears the fixed bar. */
    padding: clamp(14px, 3.8cqw, 22px) clamp(20px, 6.5%, 46px) calc(20px + env(safe-area-inset-bottom, 0px));
    flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; justify-content: flex-start; gap: clamp(14px, 3.8cqw, 22px);
    container-type: inline-size;   /* the query window; the text sizes in cqw against it */
  }
  /* text grows/shrinks with the window width (cqw). cqw is PROPORTIONAL, so "Radioactive Package"
     keeps to one line at every width. Generous max/min just guard the extremes (tiny phone / tablet). */
  .wel-eyebrow { font-size: var(--type-wel-eyebrow); line-height: var(--lh-solid); margin: 0; }
  .wel-title { font-size: var(--type-wel-title); line-height: var(--lh-tight); margin: 0; }
  .wel-subtitle { font-size: var(--type-wel-subtitle); line-height: var(--lh-subtitle); margin: 0; }
}

/* ===== Scenario — a decision on a persistent scene, with INLINE feedback ===========
   Demo-only template reproduced faithfully from tokens. Two columns: a reference SCENE stays
   visible on the left while the learner picks a single action on the right; feedback appears
   INLINE beneath the options (not a modal). Retry-until-correct. Classes are `.scn-` (NOT `.sc-`,
   which the Scorecard owns). Shares the floating Submit/Try-again (.tpl-submit) — .scn is
   position:relative to anchor it. Desktop 2-column here; tablet/phone stack below. */
.tpl-page--scenario { padding: var(--space-p8); }
.scn {
  position: relative; height: 100%; min-height: 0;
  display: grid; grid-template-columns: 1.15fr 1fr;
  background: var(--surface-chrome);
  border: var(--border-hairline) solid var(--white-a10);
  border-radius: var(--radius-card); overflow: hidden;
}
.scn-scene { position: relative; overflow: hidden; background: var(--bar-deep); }
.scn-scene-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; }
/* No-hotspot scenario scene (no observe stage): show the WHOLE picture rather than cover-cropping
   it — a wide splice would otherwise lose half its evidence in a near-square panel. When hotspots
   are added the `.has-obs` aspect-locked stage takes over and this no longer applies. */
.tpl-page--scenario .scn-scene:not(.has-obs) .scn-scene-img { object-fit: contain; }

/* --- observe phase: hotspots + chips + callout (the Click-to-Reveal pattern) --- */
/* With hotspots the scene sits in an aspect-locked stage (letterboxed on the DEEP panel) so pin
   %-coordinates stay registered to the art; a plain cover image (no hotspots) skips this.

   The lock has to survive a panel of ANY proportion: Scenario's scene is a tall-ish column
   (width binds), Click to Reveal's is a wide full-page band (height binds). `width: 100%` +
   `max-height: 100%` only holds the FIRST case — when the panel is wider than the art, the
   max-height clamps the box but nothing pulls the width in, so `aspect-ratio` is overridden,
   the art cover-crops, and every pin drifts off what it points at. So the scene panel is a
   size container and the stage takes whichever of the two caps binds first (cqw/cqh are the
   panel's CONTENT box, so the padding is already excluded). Never reintroduce a bare
   `width: 100%` here. */
.scn-scene.has-obs {
  display: flex; align-items: center; justify-content: center; padding: var(--space-p10);
  container-type: size;
}
.scn-stage {
  position: relative;
  width: min(100cqw, 100cqh * var(--scn-ar, 1.7778));
  max-width: 100%;
}
.scn-stage .scn-scene-img { border-radius: var(--radius-badge); }
/* the lead-in clip (2026-09-16, clickReveal config.clip): over the scene image while it plays, faded out on
   arrival so the image - its last frame - and the pins take over; the pins and the count wait for it */
.scn-clip { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: var(--radius-badge); background: var(--bar-deep); transition: opacity var(--dur-t260) var(--ease-standard); }
.scn-scene.is-arrived .scn-clip { opacity: 0; pointer-events: none; }
.scn-scene.has-lead:not(.is-arrived) .scn-hot,
.scn-scene.has-lead:not(.is-arrived) .scn-obs-count { visibility: hidden; }
/* the stacked layout: the clip is its own block above the explorer, gone once arrived */
.cr-m-clip { display: block; width: 100%; height: auto; aspect-ratio: var(--scn-ar, 1.7778); object-fit: cover; background: var(--bar-deep); border-radius: var(--radius-option); margin-bottom: var(--space-p12); }
.cr-m-lead.is-arrived .cr-m-clip { display: none; }
.scn-obs-hint {
  position: absolute; top: var(--space-p16); left: var(--space-p16); z-index: var(--z-l3);
  display: flex; align-items: center; gap: var(--space-p10);
  background: var(--bar-deep-a86); border-radius: var(--radius-pill); padding: var(--space-p9) var(--space-p16);
  font: var(--fw-semibold) var(--type-small)/var(--lh-solid) var(--font-ui); color: var(--text-on-navy);
}
.scn-obs-dot { width: var(--size-p10); height: var(--size-p10); border-radius: var(--radius-circle); background: var(--signal); animation: mvTwinkle var(--dur-t1600) var(--ease-in-out) infinite; }
.scn-obs-hint.is-done .scn-obs-dot { background: var(--correct); animation: none; }
/* CLICK-TO-REVEAL, DESKTOP: instructions LEFT, package RIGHT (author 2026-07-31).
   The hint used to float as a pill ON the photo, which put text over the very surfaces the
   learner is being asked to inspect, and the plate's dead countertop made the package small.
   The plate is now cropped to the package and straps (see the screen's _note), and the scene
   becomes a two-column grid: a navy instructions column beside the image.

   CSS-ONLY AND SCOPED TO `.tpl-page--click-reveal`. `.scn-obs-hint` and the ObserveScene markup
   are SHARED with the Observe and Scenario templates — restyling the component itself would have
   moved their hint pills too. The heading is a ::before on the existing hint, so no markup
   changed. Responsive is untouched: the tablet/phone explorer has its own instructions band. */
@media (min-width: 1025px /* bp:tablet +1 */) {
  .tpl-page--click-reveal .scn-scene.has-obs {
    display: grid; grid-template-columns: minmax(260px, 30%) 1fr;
    align-items: center; gap: var(--space-p28); background: var(--surface-chrome); padding: var(--space-p22);
  }
  .tpl-page--click-reveal .scn-obs-hint {
    position: static; z-index: auto; display: block;
    background: none; border-radius: 0; padding: 0;
    font: var(--fw-regular) var(--type-copy)/var(--lh-prose) var(--font-ui);
    color: var(--white-a92);
  }
  .tpl-page--click-reveal .scn-obs-hint::before {
    content: "Instructions"; display: block; margin-bottom: var(--space-p8);
    font: var(--fw-extrabold) var(--type-small)/var(--lh-title) var(--font-head);
    letter-spacing: var(--track-hotspot); text-transform: uppercase; color: var(--signal);
  }
  .tpl-page--click-reveal .scn-obs-dot { display: none; }   /* the twinkle belonged to the pill */
  .tpl-page--click-reveal .scn-obs-count { position: static; justify-self: start; margin-top: var(--space-p14); }
  /* the count is a grid item now, so it sits under the instructions rather than over the photo */
  .tpl-page--click-reveal .scn-obs-hint,
  .tpl-page--click-reveal .scn-obs-count { grid-column: 1; }
  .tpl-page--click-reveal .scn-stage { grid-column: 2; grid-row: 1 / span 3; border-radius: var(--radius-option); overflow: hidden; }
  /* The stage's width is measured against ITS COLUMN, not the whole scene (2026-09-14). The shared
     rule above says `min(100cqw, 100cqh * ar)`: on this two-column grid 100cqw is the scene's full
     width, so a wide scene (the showcase's 1920 / 821 placeholder; any landscape art) sized the
     stage wider than the 1fr column, the track grew to hold it, the centred grid overflowed both
     ways, and the instructions column slid off the left edge of the page (Tyler, on the
     showcase). `100%` resolves against the grid area, and its intrinsic contribution is auto —
     the stage's children are absolute — so the column keeps its share. The reference's
     near-square plate (776 / 729) binds on the height either way: 679 px before and after. */
  .tpl-page--click-reveal .scn-stage { width: min(100%, 100cqh * var(--scn-ar, 1.7778)); }

  /* THE DETAIL IS A SECTION, NOT A MODAL (author 2026-07-31). It used to open as a white card
     floating over the photo, dimming the very surfaces the learner is inspecting and covering
     the hotspot they just clicked. In the left column it sits under the instructions, reads at
     full size, and the package stays visible the whole time — which is the point of the screen.
     The column is top-aligned so instructions and detail stack from the top rather than
     floating mid-height. */
  .tpl-page--click-reveal .scn-scene.has-obs { align-items: start; }
  .tpl-page--click-reveal .scn-obs-hint { align-self: start; }
  .tpl-page--click-reveal .scn-callout {
    position: static; inset: auto; grid-column: 1; grid-row: 2;
    display: block; padding: 0; background: none; pointer-events: auto;
    margin-top: var(--space-p22);
  }
  .tpl-page--click-reveal .scn-callout:not(.is-open) { display: none; }
  .tpl-page--click-reveal .scn-callout.is-open { background: none; }
  .tpl-page--click-reveal .scn-callout-card {
    width: auto; max-height: none; background: none; border: 0; box-shadow: none;
    padding: 0; border-top: var(--border-hairline) solid var(--white-a16); padding-top: var(--space-p18);
  }
  .tpl-page--click-reveal .scn-callout-sub {
    font-size: var(--type-body-lg); color: var(--signal); margin-right: 0;
  }
  .tpl-page--click-reveal .scn-callout-note { font-size: var(--type-body-md); line-height: var(--lh-body); color: var(--white-a92); font-weight: var(--fw-regular); }
  .tpl-page--click-reveal .scn-callout-close { display: none; }   /* nothing to dismiss — it is in flow */
  .tpl-page--click-reveal .scn-obs-hint { font-size: var(--type-body-md); }
  .tpl-page--click-reveal .scn-obs-count { grid-row: 3; align-self: start; }
}

.scn-obs-count {
  position: absolute; top: var(--space-p16); right: var(--space-p16); z-index: var(--z-l3);
  background: var(--bar-deep-a86); border-radius: var(--radius-pill); padding: var(--space-p9) var(--space-p16);
  font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-title); text-transform: uppercase; color: var(--signal);
}
.scn-obs-count.is-done { color: var(--correct); }
.scn-hot {
  position: absolute; transform: translate(-50%, -50%);
  width: var(--hit-target); height: var(--hit-target); border: none; background: transparent; padding: 0; cursor: pointer; z-index: var(--z-l2);
}
/* the cone from a pin to the text it is about (2026-09-16, hotspots[].target): a box from the pin's centre, as long
   as the distance, as tall as the pin, rotated onto the target and clipped to a wedge - wide end under the pin, the
   point on the text; the pins paint over it (later in the DOM) */
.scn-cone { position: absolute; height: calc(var(--hit-target) - 2 * var(--space-p13)); transform-origin: 0 50%; pointer-events: none; z-index: var(--z-l1);
  background: var(--signal-a55); clip-path: polygon(0 0, 100% 50%, 0 100%); }
.scn-cone.is-seen { background: var(--correct-a14); }
/* as wide as the pin's white-ringed dot, not its hit area (Tyler, 2026-09-16: "the width of the cones should be as wide as the
   white diameter"); and hidden with the pins while a lead-in runs ("the cones should not be visible until the hotspots are") */
.tpl-page--click-reveal:not(.is-responsive) .scn-cone { height: calc(var(--size-p70) - 2 * var(--space-p21)); }
.scn-scene.has-lead:not(.is-arrived) .scn-cone { visibility: hidden; }
.scn-hot-pulse { position: absolute; inset: var(--space-p4); border-radius: var(--radius-circle); background: var(--signal-a55); animation: mvPulse var(--dur-t2000) var(--ease-out) infinite; }
.scn-hot-dot {
  position: absolute; inset: var(--space-p13); border-radius: var(--radius-circle);
  background: var(--signal); border: var(--border-w2) solid var(--white); box-shadow: 0 2px 8px var(--black-a45);
  display: flex; align-items: center; justify-content: center; color: var(--text-on-accent);
  font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-ui); animation: mvTwinkle var(--dur-pulse) var(--ease-in-out) infinite;
}
.scn-hot.is-seen .scn-hot-dot { background: var(--correct); animation: none; }
/* WCAG 2.2.2 (2026-09-06): a Click to Reveal screen runs mvPulse + mvTwinkle on every hotspot -
   8-10 of them on desktop - with no way to stop them. There is no pause control to add here
   (they are the affordance), so the reduced-motion path is the mechanism: the dot keeps its
   full-strength styling and the halo holds at a steady ring instead of pulsing. Purely
   additive - nothing changes for a viewer who has not asked for reduced motion. */
@media (prefers-reduced-motion: reduce) {
  .scn-hot-pulse { animation: none; opacity: var(--alpha-45); }
  .scn-hot-dot { animation: none; }
}
.scn-hot:focus-visible { outline: var(--border-focus) solid var(--white); outline-offset: 2px; border-radius: var(--radius-circle); }
/* Click to Reveal, DESKTOP only: hotspot circles ~60% larger (author 2026-07-25).
   Scoped so the scenario template's hotspots and the responsive explorer's
   markers keep their original size. */
.tpl-page--click-reveal:not(.is-responsive) .scn-hot { width: var(--size-p70); height: var(--size-p70); }
.tpl-page--click-reveal:not(.is-responsive) .scn-hot-pulse { inset: var(--space-p6); }
.tpl-page--click-reveal:not(.is-responsive) .scn-hot-dot { inset: var(--space-p21); font-size: var(--type-body-lg); }
/* callout = a centered MODAL over the scene panel (scrim + card). Reliable at every size,
   unlike an anchored popover (which overflowed a small phone banner). It is ALWAYS in the DOM
   (inert and invisible until `.is-open`), so Click to Reveal's phone layout can reflow this
   same element into an in-flow evidence band rather than fork a second markup path. */
.scn-callout {
  position: absolute; inset: 0; z-index: var(--z-l5); pointer-events: none;
  display: flex; align-items: center; justify-content: center; padding: var(--space-p20);
}
.scn-callout.is-open { background: var(--ink-deep-a55); pointer-events: auto; }
/* optional hotspot subtitle (author 2026-07-26): a proper light-navy header line above
   the note — desktop on the white callout card; the phone explorer's dark panel uses a
   lighter tint of the same family for contrast. */
.scn-callout-sub {
  font: var(--fw-extrabold) var(--type-copy)/var(--lh-snug) var(--font-ui);
  color: var(--text-heading-soft); margin: var(--space-p2) var(--space-p24) var(--space-p8) 0;
}
.cr-m-desc-sub {
  font: var(--fw-extrabold) var(--type-small)/var(--lh-snug) var(--font-ui);
  color: var(--signal-pale); margin: 0 0 var(--space-p6);
}
.scn-callout-card {
  position: relative; width: min(360px, 84%); max-height: 88%; overflow: auto;
  background: var(--surface-card); border: var(--border-w1-5) solid var(--ink); border-radius: var(--radius-r14);
  padding: var(--space-p20) var(--space-p22); box-shadow: 0 18px 44px var(--black-a40);
  animation: mvPopIn var(--dur-base) var(--ease-standard);
}
.scn-callout-note { font: var(--fw-semibold) var(--type-copy)/var(--lh-prose) var(--font-ui); color: var(--ink); }
.scn-callout-close {
  position: absolute; top: var(--space-p6); right: var(--space-p8); width: var(--size-p30); height: var(--size-p30);
  border: none; background: transparent; color: var(--muted); font-size: var(--type-h4); line-height: var(--lh-solid);
  cursor: pointer; border-radius: var(--radius-sm);
}
.scn-callout-close:hover { color: var(--ink); background: var(--black-a05); }

/* locked options + hint (until all evidence reviewed) */
.scn-lock-hint {
  display: flex; align-items: center; gap: var(--space-p8);
  font: var(--fw-semibold) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--white-a72);
}
.scn-lock-ico { font-size: var(--type-small); }
.scn-list.is-locked { opacity: var(--alpha-45); pointer-events: none; filter: saturate(0.6); }
.scn-choice {
  background: var(--surface-chrome); border-left: var(--border-hairline) solid var(--white-a10);
  padding: var(--space-p44) var(--space-p52); display: flex; flex-direction: column; gap: var(--space-p16); overflow: auto;
}
.scn-eyebrow {
  font: var(--fw-extrabold) var(--type-body)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--signal);
}
.scn-question {
  font-family: var(--font-display); font-weight: var(--fw-extrabold); font-size: var(--type-h1-lg);
  color: var(--text-on-navy); margin: var(--space-p2) 0 var(--space-p8); line-height: var(--lh-title);
}
.scn-list { display: flex; flex-direction: column; gap: var(--space-p13); }

/* option row — the AnswerOption RADIO primitive (round marker), reproduced from tokens */
.scn-opt {
  display: flex; align-items: center; gap: var(--space-p14);
  border: var(--border-w1-5) solid var(--border); background: var(--surface-card); border-radius: var(--radius-option);
  padding: var(--space-p14) var(--space-p16); min-height: var(--hit-target); cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-standard), background var(--dur-fast);
}
/* OPAQUE state fills — a translucent tint would let the navy column show through and turn the
   card (and its dark text) unreadable. Selected text is signal blue per the design. */
.scn-opt.is-selected  { border: var(--border-active) solid var(--signal); background: var(--signal-wash-2); }
.scn-opt.is-correct   { border: var(--border-active) solid var(--correct); background: var(--correct-wash-2); }
.scn-opt.is-incorrect { border: var(--border-active) solid var(--incorrect); background: var(--incorrect-wash-2); }
.scn-opt[tabindex="-1"] { cursor: default; }
.scn-opt:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.scn-badge {
  width: var(--size-p30); height: var(--size-p30); flex-shrink: 0; border-radius: var(--radius-badge);
  background: var(--page); color: var(--text-heading);
  font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui);
  display: flex; align-items: center; justify-content: center;
}
.scn-text { flex: 1; font: var(--fw-regular) var(--type-body)/var(--lh-bubble) var(--font-ui); color: var(--ink); }
.scn-opt.is-selected .scn-text { color: var(--signal); font-weight: var(--fw-bold); }
.scn-opt.is-correct .scn-text { font-weight: var(--fw-semibold); }
.scn-opt.is-incorrect .scn-text { font-weight: var(--fw-medium); }
.scn-marker {
  width: var(--size-p22); height: var(--size-p22); flex-shrink: 0; border-radius: var(--radius-circle);    /* round = single-select */
  border: var(--border-w2) solid var(--disabled-btn); display: flex; align-items: center; justify-content: center;
  font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui); color: var(--text-on-navy);
}
.scn-opt.is-selected .scn-marker { border-color: var(--signal); }
.scn-marker-dot { width: var(--size-p10); height: var(--size-p10); border-radius: var(--radius-circle); background: var(--signal); }
.scn-opt.is-correct .scn-marker { background: var(--correct); border-color: var(--correct); }
.scn-opt.is-incorrect .scn-marker { background: var(--incorrect); border-color: var(--incorrect); }

/* inline feedback panel (beneath the options) */
.scn-fb {
  margin-top: var(--space-p10); display: flex; gap: var(--space-p14); align-items: flex-start;
  border: var(--border-w2) solid; border-radius: var(--radius-option); padding: var(--space-p16) var(--space-p18);
  animation: mvModalIn var(--dur-t260) var(--ease-standard);
}
.scn-fb.is-correct { border-color: var(--correct); background: var(--correct-a12); }
.scn-fb.is-wrong { border-color: var(--incorrect); background: var(--incorrect-a12); }
.scn-fb-mark {
  flex-shrink: 0; width: var(--size-p30); height: var(--size-p30); border-radius: var(--radius-circle); color: var(--text-on-navy);
  font: var(--fw-extrabold) var(--type-copy)/var(--lh-solid) var(--font-ui);
  display: flex; align-items: center; justify-content: center;
}
.scn-fb.is-correct .scn-fb-mark { background: var(--correct); }
.scn-fb.is-wrong .scn-fb-mark { background: var(--incorrect); }
.scn-fb-title { font: var(--fw-extrabold) var(--type-body-lg)/var(--lh-title) var(--font-ui); color: var(--text-on-navy); margin-bottom: var(--space-p3); }
.scn-fb-body { font: var(--fw-regular) var(--type-body)/var(--lh-prose) var(--font-ui); color: var(--white-a82); }

@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--scenario { padding: 0; }
  .scn { display: flex; flex-direction: column; height: auto; min-height: 100%; border-radius: 0; border: none; }
  .scn-scene { flex: 0 0 auto; aspect-ratio: 16 / 9; max-height: 42vh; }
  .scn-choice { border-left: none; padding: var(--space-p24) clamp(16px, 4vw, 40px) var(--space-p40); overflow: visible; }
  .scn-question { font-size: var(--type-h2); }
}
@media (max-width: 600px /* bp:phone */) {
  .scn-question { font-size: var(--type-h4); }
  .scn-text { font-size: var(--type-small); }
}

/* ===== Scenario tablet/phone (scenario.js → ScenarioMobile): OBSERVE (Click-to-Reveal style)
   → gated DECIDE. Reuses the Click-to-Reveal `.cr-m-*` classes (description / numbered markers /
   numbered dot cluster) over the VERTICAL evidence image; the decision stacks below and its
   options stay LOCKED until every point is reviewed. ===== */
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--scenario.is-responsive { padding: 0; }
  .scn-m {
    display: flex; flex-direction: column; align-items: stretch;
    min-height: calc(100dvh - var(--rc-header-h, var(--chrome-flow-header-h)) - var(--chrome-flow-footer-h));
    background: var(--bar-deep);
  }
  .scn-m .cr-m-desc { flex: 0 0 auto; }   /* not the slack-absorber it is in Click-to-Reveal */

  /* top INSTRUCTION ("investigate the scenario…") — the actual question lives down with the options */
  .scn-m-top { flex: 0 0 auto; padding: var(--space-p18) clamp(16px, 5vw, 28px) var(--space-p8); }
  .scn-m-intro { margin: 0; font: var(--fw-extrabold) var(--type-h5)/var(--lh-text-tight) var(--font-ui); color: var(--text-on-navy); letter-spacing: var(--track-question); }

  /* the vertical evidence image (two stacked scenes), aspect-locked portrait, numbered markers on it */
  .scn-m-scene {
    position: relative; width: 100%; max-width: var(--size-p460); margin: var(--space-p12) auto 0;
    overflow: hidden; border-radius: var(--radius-option); background: var(--bar-deep);
    border: var(--border-hairline) solid var(--white-a10);
  }
  /* numbered dots in ONE straight row across the bottom of the image */
  .scn-m-dots {
    display: flex; justify-content: center; align-items: center; flex-wrap: wrap;
    gap: var(--space-p14); padding: var(--space-p16) var(--space-p16) var(--space-p6);
  }

  /* decision block, below the numbered dots */
  .scn-m-decide { display: flex; flex-direction: column; padding: var(--space-p4) clamp(16px, 5vw, 28px) var(--space-p28); }
  .scn-m-question { font: var(--fw-extrabold) var(--type-h4)/var(--lh-snug) var(--font-ui); color: var(--text-on-navy); margin: var(--space-p8) 0 var(--space-p16); letter-spacing: var(--track-question); }
  /* the active clue text sits BETWEEN the evidence image and the numbered dots — inset to the
     same width as the image so the two read as one evidence card */
  .scn-m .cr-m-desc {
    width: 100%; max-width: var(--size-p460); margin: var(--space-p10) auto 0; border-radius: var(--radius-option);
    border: var(--border-hairline) solid var(--white-a10);
  }
  .scn-m-options { display: flex; flex-direction: column; gap: var(--space-p12); }
  .scn-m-options-lock {
    display: flex; align-items: center; gap: var(--space-p10); padding: var(--space-p22) var(--space-p18);
    background: var(--white-a04); border: var(--border-hairline) dashed var(--white-a18); border-radius: var(--radius-option);
    font: var(--fw-semibold) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--white-a60);
  }
  .scn-m-lock-ico { font-size: var(--type-body-md); flex-shrink: 0; }
}
@keyframes mvBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(3px); } }
@keyframes mvFadeIn { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .scn-m-modal-arrow { animation: none; } .scn-m-modal { animation: none; } }

/* ===== Click to Reveal — standalone scene exploration =============================
   The mechanic is the shared observe styling (.scn-scene / .scn-stage / .scn-hot / .scn-obs-* /
   .scn-callout — the same markup Scenario's observe phase renders, from observe-parts.js). This
   block owns only what differs: the scene fills the whole page as one panel (no decision
   column), and the phone layout reflows the note into an in-flow band. Not scored; Continue
   gates until every hotspot is reviewed. */
.tpl-page--click-reveal { padding: var(--space-p8); position: relative; }   /* anchors the outro's floating Continue + modal */
.tpl-page--click-reveal .scn-scene {
  height: 100%; border-radius: var(--radius-card); border: var(--border-hairline) solid var(--white-a10);
}

@media (max-width: 1024px /* bp:tablet */) {
  /* ===== Tablet/phone EXPLORER (click-reveal.js → ClickRevealExplorer) =====
     Tiny scene hotspots are unusable on touch, so the responsive layout stacks:
     description (lighter navy) → picture-in-picture ZOOM (signal-blue comic halftone)
     → full scene with NUMBERED markers → a single row of NUMBERED dots that select each
     item (the Guided Steps dot look; the dots ARE the navigation; a row that overflows
     wraps rather than clustering into two rows — author simplified 2026-07-28). */
  .tpl-page--click-reveal.is-responsive { padding: 0; }
  /* Kill the chrome's bottom padding-reservation for this page (same trick the welcome page
     uses): .rc-content reserves 96px for the fixed nav bar, but that bar is only ~65px, so the
     leftover exposed a darker rc-shell band under the content. Instead .cr-m fills ALL the way
     to the viewport bottom (its navy paints behind the opaque bar) and its own bottom padding
     keeps the dots clear of the bar — so no band can ever show. */
  .rc-content:has(.tpl-page--click-reveal.is-responsive) { padding-bottom: 0; }
  .cr-m {
    display: flex; flex-direction: column; align-items: stretch; box-sizing: border-box;
    min-height: calc(100dvh - var(--rc-header-h, var(--chrome-flow-header-h)));
    padding-bottom: calc(70px + env(safe-area-inset-bottom, 0px));
    background: var(--bar-deep);
  }
  .cr-m-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

  /* 1) description — LIGHTER navy, clearly lighter than the chrome (#132554).
     This is the SLACK-ABSORBER: it grows to fill any leftover height so the dots land
     flush above the bottom chrome and no empty navy band shows beneath them. */
  /* 0) INSTRUCTIONS BAND — the deep navy bookend at the TOP (author 2026-07-31). It uses the
     page navy, one step darker than the description panel's #26407a below it, so the screen
     reads dark → lighter navy → halftone → photo → dark: the two navy bands bookend the stack
     and the instruction never competes with the hotspot detail for the same surface.
     `flex: 0 0 auto` — it states a fixed instruction and must not absorb slack; the description
     panel below is the slack-absorber. */
  .cr-m-instructions {
    flex: 0 0 auto; background: var(--surface-chrome); color: var(--text-on-navy);
    padding: var(--space-p16) clamp(16px, 5vw, 28px) var(--space-p18);
    border-bottom: var(--border-hairline) solid var(--white-a10);
  }
  .cr-m-instructions-title {
    font: var(--fw-extrabold) var(--type-label)/var(--lh-title) var(--font-head);
    letter-spacing: var(--track-speaker); text-transform: uppercase;
    color: var(--signal); margin-bottom: var(--space-p6);
  }
  .cr-m-instructions-body {
    margin: 0; font: var(--fw-regular) var(--type-body)/var(--lh-text-loose) var(--font-ui);
    color: var(--white-a92);
  }

  .cr-m-desc {
    flex: 1 1 auto; background: var(--navy-l2); color: var(--text-on-navy); min-height: var(--size-p96);
    padding: var(--space-p16) clamp(16px, 5vw, 28px) var(--space-p18);
    display: flex; flex-direction: column; justify-content: center;
    /* border on TOP now — the panel sits under the package image and over the number row
       (author 2026-07-31), so the rule belongs on the edge it meets the photo at. */
    border-top: var(--border-hairline) solid var(--white-a10);
  }
  .cr-m-desc-live { animation: mvCrPop var(--dur-pop) var(--ease-overshoot); }
  .cr-m-desc-eyebrow {
    display: flex; align-items: center; gap: var(--space-p9); margin-bottom: var(--space-p8);
    font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui);
    letter-spacing: var(--track-badge); text-transform: uppercase; color: var(--signal);
  }
  .cr-m-desc-num {
    width: var(--size-p22); height: var(--size-p22); flex-shrink: 0; border-radius: var(--radius-circle);
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--signal); color: var(--text-on-signal); font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-ui);
  }
  .cr-m-desc-note { font: var(--fw-medium) var(--type-body)/var(--lh-prose) var(--font-ui); color: var(--white-a92); }
  .cr-m-hint { font: var(--fw-semibold) var(--type-small)/var(--lh-prose) var(--font-ui); color: var(--white-a70); text-align: center; }

  /* 2) picture-in-picture — the signal-blue comic-strip halftone material */
  .cr-m-pip {
    flex: 0 0 auto; padding: var(--space-p14) clamp(16px, 5vw, 28px);
    display: flex; align-items: center; justify-content: center;
    background:
      var(--grad-halftone) 0 0 / 13px 13px,
      var(--grad-comic-plate);
    border-bottom: var(--border-w4) solid var(--bar-deep);
  }
  /* Row so the badge sits BESIDE the frame, not overlaid on the cutout art. */
  .cr-m-pip-row { display: flex; align-items: center; gap: var(--space-p10); width: 100%; max-width: var(--size-p420); }
  .cr-m-pip-frame, .cr-m-pip-empty {
    position: relative; flex: 1 1 auto; min-width: 0; aspect-ratio: 16 / 10;
    border-radius: var(--radius-option); overflow: hidden; border: var(--border-w3) solid var(--white);
    box-shadow: 0 6px 18px var(--navy-d12-a35);
  }
  .cr-m-pip-frame { animation: mvCrPop var(--dur-t340) var(--ease-overshoot); }
  .cr-m-pip-img { position: absolute; inset: 0; background-repeat: no-repeat; }
  .cr-m-pip-empty {
    display: flex; align-items: center; justify-content: center; text-align: center; padding: var(--space-p12);
    background: var(--navy-d13-a35); border-style: dashed; border-color: var(--white-a70);
    color: var(--white-a90); font: var(--fw-semibold) var(--type-caption)/var(--lh-text) var(--font-ui);
  }
  .cr-m-pip-badge {
    flex: 0 0 auto; width: var(--size-p28); height: var(--size-p28); border-radius: var(--radius-circle);
    display: flex; align-items: center; justify-content: center;
    background: var(--signal); color: var(--text-on-signal);
    font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui);
    border: var(--border-w2) solid var(--white); box-shadow: 0 1px 4px var(--black-a35);
  }

  /* 3) full scene with numbered markers */
  .cr-m-scene {
    position: relative; flex: 0 0 auto; width: 100%;
    height: calc(100vw / var(--scn-ar, 1.7778)); max-height: 46vh;
    overflow: hidden; background: var(--bar-deep);
  }
  .cr-m-scene-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
  .cr-m-marker {
    position: absolute; transform: translate(-50%, -50%);
    width: var(--size-p30); height: var(--size-p30); border-radius: var(--radius-circle); padding: 0; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui);
    background: var(--navy-a82); color: var(--text-on-navy); border: var(--border-w2) solid var(--white);
    box-shadow: 0 1px 5px var(--black-a40);
  }
  .cr-m-marker.is-seen { background: var(--correct-a90); }
  .cr-m-marker.is-active {
    background: var(--signal); color: var(--text-on-signal);
    transform: translate(-50%, -50%) scale(1.18);
    box-shadow: 0 0 0 5px var(--signal-a35), 0 1px 6px var(--black-a45);
  }

  /* 4) numbered dot row — the Guided Steps dot look */
  .cr-m-dots {
    flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: var(--space-p10); padding: var(--space-p16) var(--space-p16) var(--space-p8);
  }
  .cr-m-dots-row { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-p16); }
  .cr-m-dot {
    width: var(--size-p46); height: var(--size-p46); border-radius: var(--radius-circle); padding: 0; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    font: var(--fw-extrabold) var(--type-body)/var(--lh-solid) var(--font-ui);
    background: var(--surface-card); color: var(--muted); border: var(--border-w1-5) solid var(--border);
    transition: transform var(--dur-fast) var(--ease-default);
  }
  .cr-m-dot.is-seen { color: var(--correct); border-color: var(--correct); background: var(--correct-a12); }
  .cr-m-dot.is-current {
    background: var(--signal); color: var(--text-on-signal);
    border: var(--border-active) solid var(--bar-deep); box-shadow: 0 0 0 4px var(--signal-a22); transform: scale(1.06);
  }
  .cr-m-dot:focus-visible, .cr-m-marker:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--signal-a55); }

  /* (An in-flow outro Continue button lived here until 2026-07-28. Click to Reveal no
     longer renders a second "go forward" control on either layout — the chrome's own
     Continue opens the wrap-up modal instead, via clickReveal.interceptNext. Rules
     removed rather than left orphaned; git history has them if the button ever returns.) */
}

@keyframes mvCrPop {
  0% { transform: scale(.55); opacity: 0; }
  60% { transform: scale(1.05); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@media (max-width: 600px /* bp:phone */) {
  /* Tighten the stack so the whole 3-over-2 dot cluster clears the fold on a phone. */
  .cr-m-desc { min-height: var(--size-p82); padding-top: var(--space-p12); padding-bottom: var(--space-p12); }
  .cr-m-desc-note { font-size: var(--type-small); }
  .cr-m-pip { padding-top: var(--space-p10); padding-bottom: var(--space-p10); }
  .cr-m-pip-row { max-width: var(--size-p300); }
  .cr-m-scene { max-height: 33vh; }
  .cr-m-dots { padding: var(--space-p12) var(--space-p16) var(--space-p6); gap: var(--space-p8); }
  .cr-m-dots-row { gap: var(--space-p13); }
  .cr-m-dot { width: var(--size-p42); height: var(--size-p42); font-size: var(--type-small); }
}
@media (prefers-reduced-motion: reduce) {
  .cr-m-desc-live, .cr-m-pip-frame { animation: none; }
  .cr-m-dot { transition: none; }
}

/* Loud failure when a named template's component is missing from the bundle —
   an unfinished template must never silently render as something else. */
.tpl-missing {
  margin: auto;
  padding: var(--space-p28) var(--space-p36);
  max-width: var(--size-p640);
  background: var(--surface-card);
  color: var(--incorrect-deep);
  border: var(--border-w2) solid var(--incorrect-deep);
  border-radius: var(--radius-option);
  font: var(--fw-semibold) var(--type-body)/var(--lh-prose) system-ui, sans-serif;
}

/* ============================================================================
   Observe — audio-driven cinematic slideshow (renderers/templates/observe.js).
   The camera pans a flat scene plate (a multi-panel strip) while narration plays
   and reveals as text. Motion is clock-driven in JS (translateX set inline), so
   there is no CSS transition on .obs-img — scrubbing must land exactly.
   ========================================================================== */
.obs { flex: 1 1 auto; height: 100%; min-height: 0; }

/* ---- desktop: large panning scene (left) + narration/evidence (right) ---- */
.obs-desktop { display: flex; gap: var(--space-p20); padding: var(--space-p20); box-sizing: border-box; }
.obs-stage {
  flex: 1 1 auto; min-width: 0; position: relative;
  background: var(--navy-d4); border-radius: var(--radius-card); overflow: hidden;
}
/* scenes are absolute-stacked so they crossfade IN PLACE; opacity/transform come from
   segmentVisual() inline. Each centers its OWN one-panel viewport, so a scene's segment
   transform never clobbers the camera translateX on the inner img (§30.18 two-layer). */
.obs-scene { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.obs-view { height: 100%; max-height: 100%; overflow: hidden; position: relative; }  /* aspect-ratio inline = one panel (contain/desktop) */
.obs-img {
  height: 100%; width: auto; display: block;
  will-change: transform;   /* camera translateX written inline from the clock; no CSS transition */
}
/* cover mode (comic/phone+tablet): the view fills the whole band; the img crops to ONE panel via
   object-view-box (set inline) and object-fit:cover fills the band with no letterbox, no gap, and
   no neighbour peek — no measurement needed. object-position (inline) biases the crop up for faces. */
.obs-view--cover { position: absolute; inset: 0; height: auto; max-height: none; }
.obs-view--cover .obs-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* the gate chat bubble — the ONLY way forward; click / Enter / Space releases the barrier */
.obs-gate {
  position: absolute; left: 50%; bottom: 12%; transform: translateX(-50%);
  max-width: min(82%, 460px); z-index: var(--z-l5); cursor: pointer; text-align: left;
  -webkit-appearance: none; appearance: none;
  background: var(--surface-card); color: var(--ink); border: var(--border-comic) solid var(--bar-deep); border-radius: var(--radius-card);
  padding: var(--space-p14) var(--space-p18) var(--space-p12); box-shadow: 0 12px 34px var(--ink-deep-a45);
  display: flex; flex-direction: column; gap: var(--space-p2);
  animation: obs-gate-in var(--dur-pop) var(--ease-standard);
}
.obs-gate::after { content: ""; position: absolute; top: 100%; left: var(--space-p40); width: 0; height: 0; border: 12px solid transparent; border-top-color: var(--bar-deep); }
.obs-gate-speaker { font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-head); letter-spacing: var(--track-badge); text-transform: uppercase; color: var(--signal); }
.obs-gate-text { font: var(--fw-bold) var(--type-obs-gate-text)/var(--lh-bubble) system-ui, sans-serif; }
.obs-gate-hint { margin-top: var(--space-p6); font: var(--fw-bold) var(--type-nano)/var(--lh-solid) var(--font-head); letter-spacing: var(--track-badge); text-transform: uppercase; color: var(--muted); }
.obs-gate:hover, .obs-gate:focus-visible { border-color: var(--signal); outline: none; }
@keyframes obs-gate-in { from { opacity: 0; transform: translate(-50%, 10px); } to { opacity: 1; transform: translate(-50%, 0); } }
@media (prefers-reduced-motion: reduce) { .obs-gate { animation: none; } }
/* the revealing narration line, as a video-style caption over the scene */
.obs-caption {
  position: absolute; left: 0; right: 0; bottom: 0; padding: var(--space-p28) var(--space-p20) var(--space-p16);
  background: var(--grad-caption-scrim);
  pointer-events: none;
}
.obs-panel { flex: 0 0 min(340px, 34%); display: flex; flex-direction: column; gap: var(--space-p14); overflow-y: auto; }
.obs-eyebrow {
  font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-head); letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--signal);
}
.obs-heading { margin: 0; font: var(--fw-extrabold) var(--type-h4)/var(--lh-title) var(--font-head); color: var(--text-heading); letter-spacing: var(--track-drawer-title); }

/* ---- the transcript (shared desktop caption + responsive comic copy) ---- */
.obs-transcript { margin: 0; font: var(--fw-semibold) var(--type-obs-transcript)/var(--lh-prose) system-ui, sans-serif; color: var(--text-on-navy); }
.obs-caption .obs-transcript { text-shadow: 0 1px 3px var(--black-a50); }
.obs-transcript-name { display: block; font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-head); letter-spacing: var(--track-badge); text-transform: uppercase; color: var(--signal); margin-bottom: var(--space-p4); }
.obs-caret {
  display: inline-block; width: var(--size-p2); height: 1em; margin-left: var(--space-p2); vertical-align: -0.15em;
  background: currentColor; animation: obs-blink var(--dur-t1000) var(--ease-step) infinite;
}
@keyframes obs-blink { 50% { opacity: 0; } }

/* ---- evidence: always-visible, selectable observations ---- */
.obs-evidence { background: var(--surface-card); border: var(--border-hairline) solid var(--border); border-radius: var(--radius-r14); padding: var(--space-p16) var(--space-p18); }
.obs-desktop .obs-evidence { flex: 0 1 auto; }
.obs-evidence-title { font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-head); letter-spacing: var(--track-badge); text-transform: uppercase; color: var(--text-heading); margin-bottom: var(--space-p10); }
.obs-evidence-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-p10); }
.obs-evidence-item { padding-left: var(--space-p16); position: relative; font: var(--fw-regular) var(--type-small)/var(--lh-prose) system-ui, sans-serif; color: var(--ink); }
.obs-evidence-item::before { content: ""; position: absolute; left: 0; top: 0.62em; width: var(--size-p7); height: var(--size-p7); border-radius: var(--radius-circle); background: var(--signal); }
.obs-evidence-label { font-weight: var(--fw-bold); color: var(--text-heading); }
.obs-evidence-label::after { content: " — "; color: var(--muted); font-weight: var(--fw-regular); }

/* ---- desktop THREE-COLUMN Observe (author 2026-07-20): narrator's rolling history + head (left),
   comic scene strip (center), observer's evidence + head (right). The side panels reuse the comic
   signal-blue look; the heads reuse the shared .ppt-comic-head frame. Fills the wide desktop instead
   of a narrow scene marooned in dead space. Mobile/tablet keep the .obs-comic-layout split below. ---- */
.obs-desktop3 {
  display: flex; gap: var(--space-p16); padding: var(--space-p16); box-sizing: border-box;
  height: calc(100dvh - var(--rc-header-h, var(--chrome-flow-header-h)) - var(--chrome-flow-footer-h));
}
.obs-side {
  flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: var(--space-p10);
  border-radius: var(--radius-card); padding: var(--space-p14) var(--space-p14) var(--space-p10); box-sizing: border-box; overflow: hidden;
  border: var(--border-comic) solid var(--bar-deep);
  background:
    var(--grad-halftone) 0 0 / 13px 13px,
    var(--grad-comic-plate);
}
.obs-side-label {
  flex: 0 0 auto; font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-head);
  letter-spacing: var(--track-badge); text-transform: uppercase; color: var(--text-on-navy); text-shadow: 0 1px 0 var(--bar-deep);
}
.obs-side-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: var(--space-p10); }
/* the hinge on the observer's side: Sue's "What happened next?" as a clickable bubble (the only way
   forward), styled like the desktop gate but in-flow at the top of her panel. */
.obs-side-gate {
  width: 100%; text-align: left; cursor: pointer; -webkit-appearance: none; appearance: none;
  display: flex; flex-direction: column; gap: var(--space-p3); flex: 0 0 auto;
  background: var(--surface-card); color: var(--ink); border: var(--border-comic) solid var(--bar-deep); border-radius: var(--radius-r14);
  padding: var(--space-p12) var(--space-p14); box-shadow: 0 8px 22px var(--ink-deep-a40);
  animation: obs-side-gate-in var(--dur-t480) var(--ease-standard) both;   /* clean fade + slight rise above her head */
}
@keyframes obs-side-gate-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
.obs-side-gate:hover, .obs-side-gate:focus-visible { border-color: var(--signal); box-shadow: 0 0 0 3px var(--signal-a45); outline: none; }
@media (prefers-reduced-motion: reduce) { .obs-side-gate { animation: none; } }
/* Sue's speech history: her questions as chat bubbles (the active hinge is the clickable gate). Bottom-
   anchored (margin-top:auto) so her newest question rests right above her head, growing upward. */
.obs-side-speechlog { flex: 0 0 auto; margin-top: auto; display: flex; flex-direction: column; gap: var(--space-p8); }
.obs-side-speech {
  display: flex; flex-direction: column; gap: var(--space-p2);
  background: var(--surface-card); color: var(--ink); border: var(--border-comic) solid var(--bar-deep); border-radius: var(--radius-r14);
  padding: var(--space-p10) var(--space-p14); box-shadow: 0 4px 12px var(--ink-deep-a30);
}
.obs-side-speech-name { font: var(--fw-extrabold) var(--type-nano)/var(--lh-solid) var(--font-head); letter-spacing: var(--track-badge); text-transform: uppercase; color: var(--signal); }
.obs-side-speech-text { font: var(--fw-semibold) var(--type-obs-side-speech)/var(--lh-subtitle) system-ui, sans-serif; }
/* narrator history: chat-style white lines, newest at the bottom (bottom-anchored while it fits) */
.obs-side-log { display: flex; flex-direction: column; gap: var(--space-p8); }
.obs-side-scroll > .obs-side-log { margin-top: auto; }
.obs-hist-line {
  margin: 0; background: var(--surface-card); color: var(--ink); border: var(--border-comic) solid var(--bar-deep); border-radius: var(--radius-r14);
  padding: var(--space-p8) var(--space-p12); font: var(--fw-semibold) var(--type-obs-hist-line)/var(--lh-text) system-ui, sans-serif;
  box-shadow: 0 4px 12px var(--ink-deep-a30);
}
/* the observer's "What we can see" sits in its OWN darker card so the white text reads clearly
   against the bright signal-blue panel (author 2026-07-20) */
.obs-side-card {
  background: var(--navy-d7); border: var(--border-hairline) solid var(--on-navy-track);   /* OPAQUE navy — solid contrast for the signal-blue title + white body (a11y) */
  border-radius: var(--radius-r14); padding: var(--space-p14) var(--space-p16); display: flex; flex-direction: column; gap: var(--space-p10);
}
/* observer evidence: white bullets on the dark card. Each bullet starts hidden and fades in when it
   is revealed (one per beat as Loretta narrates the scene) — .is-revealed toggles the opacity. */
.obs-side-evidence { padding-right: var(--space-p2); }
.obs-side-evidence .obs-evidence-item { color: var(--signal-wash-3); opacity: 0; transition: opacity var(--dur-t600) var(--ease-default); }
.obs-side-evidence .obs-evidence-item.is-revealed { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .obs-side-evidence .obs-evidence-item { transition: none; } }
.obs-side-evidence .obs-evidence-item::before { background: var(--surface-card); }
.obs-side-evidence .obs-evidence-label { color: var(--text-on-navy); }
.obs-side-evidence .obs-evidence-label::after { color: var(--white-a70); }
/* the "What we can see" TITLE: signal blue and larger than the body text */
/* WCAG 1.4.3 (2026-09-06): this label sits on the DARK card, where --signal is fine; the two
   labels that failed are the ones directly on the Signal-Blue halftone (below). Left as-is. */
.obs-side-card .obs-side-label { color: var(--signal); font-size: var(--type-obs-side-label); }
/* the evidence BODY crossfades on a scene change (after clicking the hinge): 0.75s out, 0.75s in = 1.5s */
.obs-evidence-fade { transition: opacity var(--dur-t750) var(--ease-default); }
.obs-evidence-fade.is-out { opacity: 0; }
.obs-evidence-fade.is-in { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .obs-evidence-fade { transition: none; } }
/* the big head "bookend" owns the bottom THIRD of the panel. Sized off the panel (flex-basis), not
   viewport units, so it is correct inside the player's scaled desktop canvas. The head fills that
   third by HEIGHT (leaving room for the name), width follows its square aspect and is capped at the
   panel width so a narrow panel never overflows. */
.obs-side-head {
  flex: 0 0 33%;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: var(--space-p4); padding-bottom: var(--space-p2);
}
.obs-side-head .ppt-comic-head { height: calc(100% - 24px); width: auto; max-width: 100%; }
.obs-side-head .ppt-comic-name {
  font: var(--fw-extrabold) var(--type-obs-side-name)/var(--lh-solid) var(--font-head); letter-spacing: var(--track-title);
  /* WCAG 1.4.3 (2026-09-06): white on the Signal-Blue halftone is 2.88:1 at <=14px. Navy on the
     same fill is 5.13:1. The text-shadow was the previous mitigation - it helps perceptually but
     WCAG credits no shadow - and a dark shadow under dark text only muddies it. The shadow is
     inherited from the BASE .ppt-comic-name rule (screens.css:101), which the responsive comic
     circles still need, so it is cancelled HERE rather than removed there. */
  text-transform: uppercase; color: var(--text-heading); text-shadow: none;
}
/* the observer (silent) head reads a touch quieter than the speaking narrator — but brightens to full
   when she's the one asking at a hinge (is-speaking). */
.obs-side--observer .obs-side-head .ppt-comic-head { filter: saturate(.82) brightness(.88); }
.obs-side--observer .obs-side-head.is-speaking .ppt-comic-head { filter: none; }

/* center: the portrait comic scene strip, filling the column height */
.obs-center { flex: 0 0 auto; display: flex; align-items: center; justify-content: center; min-width: 0; }
.obs-strip {
  height: 100%; aspect-ratio: var(--obs-strip-ar, 0.6); width: auto; max-width: 100%;
  position: relative; overflow: hidden; border-radius: var(--radius-card);
  background: var(--navy-d4); border: var(--border-comic) solid var(--bar-deep);
}

/* ---- responsive: comic split (talking head + transcript on top, scene below) ---- */
/* fill between the header and bottom bar EXPLICITLY (the same value the intro comic uses) —
   height:100% does not resolve inside the ResponsiveShell, which collapsed the scene band. */
/* The scene band is WIDTH-DRIVEN on tablet/phone (author 2026-07-29), which is the whole fix
   for "why does this have to zoom in".
   It used to be height-driven: this column was locked to the viewport and the band took
   whatever was left after the 45% head panel, so the FRAME's aspect was a different number on
   every device (1.37 on a phone) while the art is 0.755 portrait — a 45% mismatch, taken
   equally off the top and bottom, which is what removed Sue's head. The tab-interaction images
   never did that because their frame has a FIXED aspect and their height follows their width
   (.tab-panel-img), so the zoom is identical at every viewport. This adopts that: the band
   takes a fixed aspect (--obs-scene-aspect, set from the tightest protect-rect in the scene so
   every panel fits with no letterbox bars) and the page is allowed to scroll, exactly as the
   tab page already does ("A long tab set just makes the rail taller — the page scrolls; that's
   fine on a phone"). Desktop is untouched and still does not scroll: it uses .obs-desktop3. */
.obs-comic-layout { display: flex; flex-direction: column; min-height: calc(100dvh - var(--rc-header-h, var(--chrome-flow-header-h)) - var(--chrome-flow-footer-h)); }
/* The comic panel IS the intro-01 .ppt-comic (two heads, speaker grows, floating chat bubble) —
   used as-is so Observe gets the exact intro-01 proportions. Only its flex share is set here. */
/* Unchanged in size — still exactly 45% of the old viewport-locked box, so the name still rests
   ~midway on the dotted line and this matches intro-01/intro-02. Stated as an explicit height
   now rather than a flex percentage, because the column's height became indefinite (min-height,
   above) and a percentage flex-basis against an indefinite height falls back to content.
   STICKY, because the scene below it can now be taller than the viewport: this panel holds the
   typing bubble AND the hinge bubble that is the only way forward, so it must not be possible to
   scroll them out of view. */
.obs-comic-panel {
  flex: 0 0 calc((100dvh - var(--rc-header-h, var(--chrome-flow-header-h)) - var(--chrome-flow-footer-h)) * 0.45);
  padding-bottom: 1.1vh;
  position: sticky; top: 0; z-index: var(--z-l6);
}
/* a hinge question is a CLICKABLE chat bubble above the speaker's head (the only way forward) */
.obs-comic-gate { cursor: pointer; font: inherit; text-align: left; }
.obs-comic-gate:hover, .obs-comic-gate:focus-visible { border-color: var(--signal); box-shadow: 0 0 0 3px var(--signal-a45); outline: none; }
.obs-comic-gate .obs-gate-hint { display: block; margin-top: var(--space-p4); font-size: var(--type-gate-hint); font-weight: var(--fw-bold); color: var(--signal); text-transform: uppercase; letter-spacing: var(--track-caption); }
/* Width-driven: the aspect is fixed and the height follows, so the zoom level is identical at
   every viewport width. --obs-scene-aspect comes from the screen's scene (observe.js), set to
   the tightest protect-rect aspect among that scene's panels so all of them fit uncropped. */
.obs-comic-scene {
  flex: 0 0 auto; position: relative; overflow: hidden; background: var(--navy-d4);
  aspect-ratio: var(--obs-scene-aspect, 1);
}
/* evidence as a slide-over DRAWER on tablet + phone (mirrors the Category Match reference
   drawer). Only rendered in the responsive comic layout. The sliding PANEL holds the tab (on
   its left edge) + the white drawer body, so the tab peeks at the viewport edge when closed and
   MOVES IN WITH the drawer when open. A separate scrim catches a tap outside to close.
   Width: TABLET matches the References/Menu drawer (min(52vw,420px)); PHONE is ~60% so the
   uncovered remainder stays a tap-to-close target. */
.obs-ev { display: contents; }                 /* layout-inert wrapper; children anchor to the viewport */
.obs-ev-scrim { position: fixed; inset: 0; z-index: var(--z-template-drawer-scrim); background: var(--navy-d8-a42); }
.obs-ev-panel {
  position: fixed; z-index: var(--z-template-drawer); top: var(--rc-header-h, var(--chrome-flow-header-h)); bottom: 0; right: 0;
  width: min(52vw, 420px);                      /* tablet: same as the References/Menu drawer */
  transform: translateX(100%);
  transition: transform var(--dur-t240) var(--ease-standard);
  overflow: visible; pointer-events: none;      /* the tab peeks past the panel's left edge */
}
.obs-ev-panel.is-open { transform: translateX(0); }
@media (max-width: 600px /* bp:phone */) { .obs-ev-panel { width: min(60vw, 360px); } }   /* phone: ~60% tap-to-close */
.obs-ev-tab {
  /* rides the TOP of the scene (slideshow) band, clear of the comic panel above it: the panel is
     45% of the comic-layout, so the scene starts at 45% down (the panel is fixed from the header). */
  position: absolute; left: var(--space-n44); top: calc((100dvh - var(--rc-header-h, var(--chrome-flow-header-h)) - var(--chrome-flow-footer-h)) * 0.45 + 12px);
  width: var(--hit-target); margin: 0; border: 0; padding: var(--space-p16) var(--space-p8);
  /* Deep Navy on Signal Blue = 5.13:1 (white would fail WCAG 1.4.3/1.4.11) */
  background: var(--signal); color: var(--text-on-signal);
  font: var(--fw-bold) var(--type-label)/var(--lh-solid) system-ui, sans-serif; letter-spacing: var(--track-tab);
  writing-mode: vertical-rl; text-orientation: mixed;
  border-radius: var(--radius-r-10-0-0-10); box-shadow: -3px 0 10px var(--black-a28);
  cursor: pointer; pointer-events: auto;
}
.obs-ev-tab:focus-visible { outline: var(--border-focus) solid var(--white); outline-offset: 2px; }
.obs-ev-drawer {
  height: 100%; overflow-y: auto; pointer-events: auto;
  background: var(--surface-card); box-shadow: -8px 0 24px var(--navy-d8-a35); padding: var(--space-p18) var(--space-p18) var(--space-p24);
}
.obs-ev-drawer-head { margin: 0 0 var(--space-p12); font: var(--fw-extrabold) var(--type-body-md)/var(--lh-title) var(--font-head); color: var(--text-heading); }
.obs-ev-drawer-head:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 3px; border-radius: var(--radius-r4); }
.obs-ev-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-p14); }
.obs-ev-item { font: var(--fw-regular) var(--type-small)/var(--lh-prose) system-ui, sans-serif; color: var(--ink); padding-left: var(--space-p14); position: relative; }
.obs-ev-item::before { content: ""; position: absolute; left: 0; top: .62em; width: var(--size-p7); height: var(--size-p7); border-radius: var(--radius-circle); background: var(--signal); }
.obs-ev-item-name { font-weight: var(--fw-bold); color: var(--text-heading); }
.obs-ev-item-name::after { content: " — "; color: var(--muted); font-weight: var(--fw-regular); }
@media (prefers-reduced-motion: reduce) { .obs-ev-drawer { transition: none; } }

/* ===== Cover — deliverable cover sheet (DELIVERABLE COPY ONLY) ====================
   Full-stage Deep Navy title card shown before everything else so this running
   copy is unmistakably the deliverable. Style follows the design tokens: signal
   eyebrow, Archivo 800 uppercase title, one signal accent (the badge). ========== */
.tpl-page--cover {
  display: flex; align-items: center; justify-content: center;
  min-height: 100%; padding: clamp(24px, 5vw, 64px);
  background: var(--surface-chrome);
  text-align: center;
}
.cover-stack { display: flex; flex-direction: column; align-items: center; gap: var(--space-p18); }
.cover-eyebrow {
  font: var(--fw-extrabold) var(--type-cover-eyebrow)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-cover-eyebrow); text-transform: uppercase;
  color: var(--signal);
}
.cover-title {
  margin: 0;
  font: var(--fw-extrabold) var(--type-cover-title)/var(--lh-compact) var(--font-ui);
  letter-spacing: var(--track-tight); text-transform: uppercase;
  color: var(--text-on-navy);
}
.cover-badge {
  margin-top: var(--space-p10);
  font: var(--fw-extrabold) var(--type-cover-badge)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-cover-badge); text-transform: uppercase;
  color: var(--text-on-signal);
  background: var(--signal);
  border-radius: var(--radius-pill);
  padding: var(--space-p14) var(--space-p36);
}

/* ===== Lecture Accordion — character lectures beside a compact accordion ==========
   Variant of the Explore accordion (.acc) requested by the author 2026-07-24. Three
   differences, all here in CSS + lecture-accordion.js; the plain accordion is untouched:
     1. the picture is EMBEDDED in each open panel (.lacc-body-img) — no shared side frame;
     2. the clickable title row is SLIMMER vertically (compact padding + type), so more rows
        fit and the open panel gets the vertical room;
     3. a CHARACTER COLUMN on the left holds a standing figure addressing the camera.
   Colors/tokens mirror the accordion: DEEP #0E1C42 surface, signal-blue accents, white text. */
/* FULL-BLEED ROOM (author 2026-07-24): the room IS the whole content zone — zero page
   padding, no card frame — so the light-navy wall flexes to cover the entire background
   and the darker stage navy can never show as a strip below or around the interaction. */
.tpl-page--lecture-accordion { padding: 0; }
/* The page reads as a ROOM the lecturer stands in (author 2026-07-24): a LIGHT NAVY back
   wall, and a floor rectangle across the bottom whose gradient is DARKER AT ITS TOP —
   i.e. in shadow where floor meets wall — lightening toward the viewer. The floor spans
   the full width (it is the bottom of the player area, not just the character column), so
   the character column and the accordion share one continuous room. */
.lacc {
  position: relative;                                  /* anchors the ::after floor */
  display: flex;
  gap: 0;
  min-height: 100%;
  height: 100%;
  background: var(--navy-l2);                                 /* back wall — light navy */
}
.lacc::after {                                         /* the floor */
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 24%; pointer-events: none; z-index: var(--z-l0);
  background: var(--grad-floor);
  box-shadow: inset 0 1px 0 var(--white-a06); /* faint wall/floor seam */
}
/* everything the learner reads or looks at sits ON the room, never under the floor */
.lacc-stage, .lacc-list-col { position: relative; z-index: var(--z-l1); }

/* --- LEFT: the lecturer's column. The figure stands on the floor of the column
       (bottom-aligned) and keeps its proportions (contain), so any pose height works. --- */
.lacc-stage {
  width: 26%;
  min-width: var(--size-p180);
  flex-shrink: 0;
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  /* bottom padding seats her feet INSIDE the floor band (floor = bottom 24%), so she reads as
     standing ON the floor rather than balanced on its front edge. */
  padding: 0 var(--space-p8) 7%;
  background: transparent;              /* the room (wall + floor) shows through */
  border-right: var(--border-hairline) solid var(--white-a08);
}
.lacc-character {
  display: block;
  max-width: 100%;
  max-height: 96%;
  width: auto; height: auto;
  object-fit: contain; object-position: bottom center;
  filter: drop-shadow(0 18px 16px var(--black-a35));
}

/* --- RIGHT: the compact accordion column --- */
.lacc-list-col {
  flex: 1;
  min-width: 0;
  padding: var(--space-p28) var(--space-p40) var(--space-p30);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}
/* CENTER the interaction (eyebrow + title + accordion) in the room (author 2026-07-24):
   the flex pseudos absorb the leftover height equally (safe centering — flex-basis 0, so
   when content is taller than the column they collapse to 0 and it simply scrolls); the
   max-width + auto margins center the group horizontally in its column. */
.lacc-list-col::before, .lacc-list-col::after { content: ""; flex: 1 0 0; }
.lacc-eyebrow, .lacc-heading, .lacc-list {
  width: 100%; max-width: var(--size-p860); margin-left: auto; margin-right: auto;
}
.lacc-heading { margin-top: var(--space-p5); margin-bottom: var(--space-p14); }
.lacc-eyebrow {
  font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--signal);
}
.lacc-heading {
  font-family: var(--font-display);
  font-weight: var(--fw-extrabold); font-size: var(--type-h1); line-height: var(--lh-heading);
  color: var(--text-on-navy); margin: var(--space-p5) auto var(--space-p14);   /* auto sides keep the centered-group margins */
}
.lacc-list { display: flex; flex-direction: column; }
.lacc-item { border-bottom: var(--border-hairline) solid var(--white-a12); }

/* SLIMMER title row: ~10px vertical padding (vs the accordion's 20px) and a smaller
   number chip + title, so a five-row lecture still fits without scrolling. */
.lacc-head {
  width: 100%; text-align: left; background: transparent; border: none; cursor: pointer;
  padding: var(--space-p10) var(--space-p4); display: flex; align-items: center; gap: var(--space-p12);
}
.lacc-num {
  width: var(--size-p24); height: var(--size-p24); flex-shrink: 0; border-radius: var(--radius-circle);
  display: flex; align-items: center; justify-content: center;
  font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui);
  background: var(--white-a10); color: var(--white-a70);
}
/* Deep Navy on Signal Blue = 5.13:1 (white would fail WCAG 1.4.3/1.4.11) */
.lacc-item.is-open .lacc-num { background: var(--signal); color: var(--text-on-signal); }
.lacc-title { flex: 1; font: var(--fw-bold) var(--type-copy)/var(--lh-title) var(--font-ui); color: var(--text-on-navy); }
.lacc-toggle {
  font-size: var(--type-h3); font-weight: var(--fw-regular); color: var(--signal);
  line-height: var(--lh-solid); width: var(--size-p20); text-align: center; flex-shrink: 0;
}
.lacc-head:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; border-radius: var(--radius-sm); }

/* The open panel: picture EMBEDDED above its text (this is the defining difference). */
.lacc-body {
  padding: 0 var(--space-p4) var(--space-p16) var(--space-p36);
  animation: mvModalIn var(--dur-t260) var(--ease-standard);   /* keyframes in player.css */
}
.lacc-body-img {
  display: block; width: 100%; max-width: var(--size-p560);
  aspect-ratio: 8 / 3; object-fit: cover;
  border-radius: var(--radius-option); margin: var(--space-p2) 0 var(--space-p12);
  background: var(--grey-6); border: var(--border-hairline) solid var(--grey-10);
  box-shadow: inset 0 2px 22px var(--navy-a14);
  animation: mvImageFade var(--dur-t400) var(--ease-in-out);
}
.lacc-body-text {
  font: var(--fw-regular) var(--type-copy)/var(--lh-body) var(--font-ui);
  color: var(--white-a82);
}

/* The in-panel lecturer stage is DESKTOP-HIDDEN: on desktop the left column is the
   lecturer, so this duplicate never shows above 1024px. */
/* The in-panel lecturer stage and the visuals grid are RESPONSIVE-ONLY: on desktop the
   lecturer is the page-level left column, so this duplicate never shows above 1024px and
   the illustration alone fills the visuals row. */
.lacc-body-stage { display: none; }
.lacc-visuals { display: block; }

/* RESPONSIVE (matches the other template reflows: 1024 -> tablet/phone). The standing left
   column cannot work on a narrow screen, so the ROOM AND THE LECTURER MOVE INSIDE EACH
   ACCORDION ITEM: one rounded card per point, whose trigger row is its top section and whose
   panel holds lecturer (1/3) beside illustration (2/3) with the description full-width beneath.
   Collapsed, only the trigger row shows. CONTENT-DRIVEN: no fixed heights anywhere - the
   illustration's aspect-ratio sets the row height and the lecturer stage stretches to match,
   so nothing can overflow its card (author spec 2026-07-24). */
@media (max-width: 1024px /* bp:tablet */) {
  /* kill the chrome's bottom padding-reservation (it shows as a dark strip above the bar);
     .lacc instead fills to the viewport bottom and reserves the bar space itself, so the
     LIGHT NAVY wall runs all the way down (author 2026-07-24). */
  .rc-content:has(.tpl-page--lecture-accordion) { padding-bottom: 0; }
  .tpl-page--lecture-accordion { background: var(--navy-l2); }
  .lacc {
    flex-direction: column; height: auto;
    min-height: calc(100dvh - var(--rc-header-h, var(--chrome-flow-header-h)));
    box-sizing: border-box;
    padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));   /* clears the fixed bar */
    background: var(--navy-l2);               /* the wall, full height — no dark strip */
  }
  .lacc::after { display: none; }      /* the room moves into each card; no page-level floor */
  .lacc-stage { display: none; }       /* desktop-only lecturer column */
  .lacc-list-col { padding: var(--space-p20) clamp(14px, 4vw, 28px) var(--space-p26); overflow-x: hidden; }
  .lacc-heading { font-size: var(--type-h3); }
  .lacc-list { gap: var(--space-p10); }

  /* THE CARD: the whole collapsible item, trigger + panel inside one surface. */
  .lacc-item {
    width: 100%; min-width: 0;
    border: var(--border-hairline) solid var(--on-navy-track);
    border-radius: var(--radius-card);
    background: var(--navy-d5-a55);   /* darker than the light-navy wall, so cards read */
    overflow: hidden;                  /* keeps the card its own clipping boundary */
  }
  .lacc-head {
    width: 100%; min-width: 0;
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center; gap: var(--space-p12);
    padding: var(--space-p14) var(--space-p16); text-align: left;
  }
  .lacc-title { font-size: var(--type-body); min-width: 0; overflow-wrap: anywhere; }
  .lacc-body { display: block; padding: 0 var(--space-p16) var(--space-p18); }

  /* lecturer 1/3 | illustration 2/3 — minmax(0,*) is what lets both shrink instead of
     forcing the row wider than the card (the cause of the earlier right-edge clipping). */
  .lacc-visuals {
    display: grid;
    grid-template-columns: minmax(76px, 1fr) minmax(0, 2fr);
    gap: var(--space-p12); align-items: stretch;
  }
  .lacc-body-stage, .lacc-body-img { width: 100%; min-width: 0; border-radius: var(--radius-option); margin: 0; }

  .lacc-body-stage {
    display: flex; align-items: flex-end; justify-content: center;
    position: relative; overflow: hidden;
    height: auto; min-height: 0;
    border: var(--border-hairline) solid var(--white-a10);
    background: var(--grad-room-wall);
    animation: mvImageFade var(--dur-t400) var(--ease-in-out);
  }
  .lacc-body-stage::after {            /* the floor, darker at its top where it meets the wall */
    content: ""; position: absolute; left: 0; right: 0; bottom: 0;
    height: 26%; pointer-events: none; z-index: var(--z-l0);
    background: var(--grad-floor);
    box-shadow: inset 0 1px 0 var(--white-a06);
  }
  .lacc-body-stage .lacc-character {
    position: relative; z-index: var(--z-l1);
    display: block; width: auto; max-width: 100%;
    height: auto; max-height: 100%;
    margin-bottom: 6%;                 /* seats her feet in the floor band */
  }

  .lacc-body-img {
    display: block; height: auto;
    aspect-ratio: 16 / 10;             /* content-driven: this sets the row height */
    object-fit: cover;
  }

  .lacc-body-text { font-size: var(--type-body); margin: var(--space-p14) 0 0; }
}

/* the phone talking-head float is TABLET/DESKTOP-HIDDEN (phone block below shows it) */
.lacc-head-float { display: none; }

/* ===== PHONE (<=600px, the project's phone line) — author sketch 2026-07-24:
   the side-by-side row squeezes the illustration too much on a phone, so the card
   restacks: title row, then the illustration FULL-WIDTH (the sketch's blue box), then
   the description with a small TALKING-HEAD square of the lecturer floated at its
   top-left (the red box) and the text wrapping around and below it. Tablet (601–1024)
   keeps the 1/3 + 2/3 row above. ===== */
@media (max-width: 600px /* bp:phone */) {
  .lacc-visuals { display: block; }
  .lacc-body-stage { display: none; }          /* the standing figure is a tablet layout */
  .lacc-body-img { width: 100%; }

  .lacc-head-float {
    display: block; float: left;
    width: clamp(72px, 24vw, 96px); aspect-ratio: 1;
    margin: var(--space-p3) var(--space-p12) var(--space-p4) 0;
    border-radius: var(--radius-option); overflow: hidden; position: relative;
    border: var(--border-hairline) solid var(--on-navy-track);
    /* the same little room: light-navy wall with the floor shadow at the base */
    background: var(--grad-room-wall-float);
  }
  .lacc-head-float img {
    display: block; width: 100%; height: 100%;
    object-fit: contain; object-position: center bottom;
  }
  /* fallback when a character has no head crop: show the figure's upper body instead
     (cover from the top edge crops to head-and-shoulders rather than a tiny full figure) */
  .lacc-head-float.is-figure img { object-fit: cover; object-position: top center; }
}

@media (prefers-reduced-motion: reduce) {
  .lacc-body, .lacc-body-img, .lacc-body-stage { animation: none; }
}

/* ===== Flip Cards — flippable term cards (author spec 2026-07-24) =================
   Resting face: LIGHT NAVY card with a SIGNAL-BLUE band carrying the title in DARK
   NAVY (navy-on-signal = 5.13:1). Flip (3D rotateY): WHITE interior with a LIGHT-NAVY
   boundary all around — image top half, text bottom half. Viewed = flipped at least
   once; the page gates until all viewed (flip-card.js). Desktop 3 columns; tablet 2;
   phone 1. Reduced motion: instant swap, no rotation. */
.tpl-page--flip-card { display: flex; }
.fc {
  margin: auto; width: 100%; max-width: var(--size-p1180);
  padding: var(--space-p18) clamp(16px, 3vw, 40px) var(--space-p26);
  display: flex; flex-direction: column;
}
.fc-eyebrow {
  font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--signal);
}
.fc-heading {
  font-family: var(--font-display);
  font-weight: var(--fw-extrabold); font-size: var(--type-h1); line-height: var(--lh-heading);
  color: var(--text-on-navy); margin: var(--space-p5) 0 var(--space-p2);
}
.fc-hint { margin: 0 0 var(--space-p14); font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--white-a65); }
.fc-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-p16); }

.fc-card {
  position: relative; border: none; background: transparent; padding: 0; cursor: pointer;
  aspect-ratio: 4 / 3; perspective: 1000px; text-align: left;
}
.fc-card:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 3px; border-radius: var(--radius-r14); }
.fc-inner {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform var(--dur-flip-delay) var(--ease-b-3-7-3-1);
}
.fc-card.is-flipped .fc-inner { transform: rotateY(180deg); }
.fc-face {
  position: absolute; inset: 0; border-radius: var(--radius-r14); overflow: hidden;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
/* resting face — light navy, signal band, dark navy title */
.fc-front {
  background: var(--navy-l2);
  border: var(--border-hairline) solid var(--on-navy-track);
  display: flex; align-items: center; justify-content: center;
}
.fc-band {
  width: 100%; padding: var(--space-p14) var(--space-p18); text-align: center;
  background: var(--signal); color: var(--text-on-signal);   /* dark navy title on signal */
  font: var(--fw-extrabold) var(--type-fc-band)/var(--lh-title) var(--font-ui);
  letter-spacing: var(--track-dialogue);
}
.fc-flip-hint {
  position: absolute; right: var(--space-p12); bottom: var(--space-p8);
  font: var(--fw-regular) var(--type-h2)/var(--lh-solid) var(--font-ui); color: var(--white-a55);
}
.fc-card.is-seen .fc-flip-hint { color: var(--correct); }
/* flipped face — white interior, LIGHT-NAVY boundary all the way around */
.fc-back {
  transform: rotateY(180deg);
  background: var(--surface-card); border: var(--border-w3) solid var(--navy-l2);
  display: flex; flex-direction: column;
}
/* 62/38 image/text split (author 2026-07-25; was 50/50) — the bodies are one short
   sentence, so the image gets the taller share. Body keeps overflow-y for long text. */
.fc-media { flex: 1 1 62%; min-height: 0; background: var(--page); position: relative; }
.fc-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.fc-img--contain { object-fit: contain; padding: var(--space-p6); box-sizing: border-box; }
.fc-body {
  flex: 1 1 38%; min-height: 0; overflow-y: auto;
  padding: var(--space-p10) var(--space-p14) var(--space-p12);
  font: var(--fw-regular) var(--type-caption)/var(--lh-text-loose) var(--font-ui); color: var(--ink);
}
/* DESKTOP: bigger card copy (author 2026-07-31). 14px was sized for the narrow tablet card
   and carried over to desktop unchanged, where the cards are far wider and the text read small
   against its own artwork. 17px matches the course's normal body size — the same size the
   accordion and guided-steps panels use — so a flip card no longer reads as fine print.
   The hint line moves with it so the two stay in proportion. */
@media (min-width: 1025px /* bp:tablet +1 */) {
  .fc-body { font-size: var(--type-copy); line-height: var(--lh-prose); padding: var(--space-p12) var(--space-p18) var(--space-p14); }
  .fc-hint { font-size: var(--type-body); }
}
@media (max-width: 1024px /* bp:tablet */) { .fc-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px /* bp:phone */)  { .fc-grid { grid-template-columns: 1fr; } .fc-heading { font-size: var(--type-h3); } }
@media (prefers-reduced-motion: reduce) {
  .fc-inner { transition: none; }
}

/* ===== Tabs — folder with file-tabs (author spec 2026-07-24) ======================
   Desktop: tabs run across the TOP of the page like a file folder; the active tab
   merges seamlessly into the folder body below (same surface, no seam). Clicking a
   tab shows that layer — single-open, like the accordion. Tablet/phone (<=1024):
   the tabs move to the LEFT-HAND SIDE as a vertical rail. */
.tpl-page--tabs { display: flex; position: relative; }   /* anchors the conversation modal */
.tabs {
  margin: auto; width: 100%; max-width: var(--size-p1180);
  padding: var(--space-p18) clamp(16px, 3vw, 40px) var(--space-p26);
  display: flex; flex-direction: column;
}
.tab-eyebrow {
  font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--signal);
}
.tab-heading {
  font-family: var(--font-display);
  font-weight: var(--fw-extrabold); font-size: var(--type-h1); line-height: var(--lh-heading);
  color: var(--text-on-navy); margin: var(--space-p5) 0 var(--space-p16);
}
.tab-instruction {
  font: var(--fw-regular) var(--type-copy)/var(--lh-prose) var(--font-ui);
  color: var(--white-a80);
  margin: var(--space-n8) 0 var(--space-p16);
}
.tab-folder { display: flex; flex-direction: column; min-height: 0; }
.tab-strip { display: flex; gap: var(--space-p6); padding: 0 var(--space-p14); }
.tab-tab {
  border: var(--border-hairline) solid var(--white-a16); border-bottom: none;
  border-radius: var(--radius-r-12-12-0-0);
  padding: var(--space-p12) var(--space-p22) var(--space-p10); cursor: pointer;
  background: var(--navy-l3);                                  /* resting tab */
  color: var(--white-a85);
  font: var(--fw-bold) var(--type-small)/var(--lh-solid) var(--font-ui);
  position: relative; top: var(--space-p1);                          /* overlaps the body border for the merge */
}
.tab-tab.is-seen::after { content: " ✓"; color: var(--correct); font-weight: var(--fw-extrabold); }
.tab-tab.is-active {
  background: var(--navy-l2); color: var(--text-on-navy);                      /* same surface as the body = folder merge */
  border-color: var(--on-navy-rule);
  z-index: var(--z-l1);
}
.tab-tab:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: -3px; }
.tab-panel {
  background: var(--navy-l2); border: var(--border-hairline) solid var(--on-navy-rule);
  border-radius: var(--radius-r14); padding: var(--space-p22) var(--space-p26);
  display: flex; gap: var(--space-p22); align-items: flex-start;
  animation: mvModalIn var(--dur-t240) var(--ease-standard);   /* the accordion's layer-in */
}
.tab-panel-img {
  flex: 0 0 40%; min-width: 0; max-width: var(--size-p480);
  aspect-ratio: 4 / 3; object-fit: cover;   /* squarer frame (author 2026-07-24), was 8:3 */
  border-radius: var(--radius-button); border: var(--border-hairline) solid var(--on-navy-track);
  background: var(--grey-6);
}
.tab-panel-text { flex: 1; min-width: 0; }
.tab-panel-title { margin: 0 0 var(--space-p8); font: var(--fw-extrabold) var(--type-lead)/var(--lh-title) var(--font-ui); color: var(--text-on-navy); }
.tab-panel-body { font: var(--fw-regular) var(--type-body)/var(--lh-body) var(--font-ui); color: var(--white-a85); }

/* tablet/phone: VERTICAL tabs on the left-hand side */
@media (max-width: 1024px /* bp:tablet */) {
  .tab-folder { flex-direction: row; align-items: stretch; }
  /* narrow rail of VERTICAL-TEXT tabs (author 2026-07-24): the label runs DOWN the tab
     (same writing-mode as the Category Match drawer tab), so the rail stays ~48px wide and
     the folder body gets the width. A long tab set just makes the rail taller — the page
     scrolls; that's fine on a phone. */
  .tab-strip { flex-direction: column; gap: var(--space-p6); padding: var(--space-p14) 0; flex: 0 0 auto; max-width: none; }
  .tab-tab {
    border: var(--border-hairline) solid var(--white-a16); border-right: none;
    border-radius: var(--radius-r-12-0-0-12);
    top: 0; left: var(--space-p1);                                   /* overlap the body's left border */
    padding: var(--space-p16) var(--space-p10); text-align: left;
    writing-mode: vertical-rl; text-orientation: mixed;   /* text runs down the tab */
    white-space: nowrap;
  }
  .tab-tab.is-active { left: var(--space-p1); }
  .tab-panel { flex-direction: column; gap: var(--space-p14); border-radius: var(--radius-r-0-14-14-14); min-width: 0; flex: 1; }
  .tab-panel-img { flex: 0 0 auto; width: 100%; max-width: 100%; }
  .tab-heading { font-size: var(--type-h3); }
}
@media (prefers-reduced-motion: reduce) {
  .tab-panel { animation: none; }
}

/* ===== Tabs: conversation modal (config.conversation) =======================
   A full-stage overlay that covers the whole interaction with a two-character
   dialogue before the learner reaches the tabs. Characters flank a central
   speech bubble; one button advances the lines and finally dismisses. */
.tab-conv-scrim {
  position: absolute; inset: 0; z-index: var(--z-conversation);
  background: var(--navy-d10-a72);
  display: flex; align-items: center; justify-content: center;
  animation: mvModalIn var(--dur-t240) var(--ease-standard);
}
.tab-conv-modal {
  position: relative;
  width: min(1120px, 92%); height: min(660px, 90%);
  background: var(--navy-l3);
  border: var(--border-hairline) solid var(--on-navy-rule);
  border-radius: var(--radius-r20);
  overflow: hidden;
  display: flex; align-items: flex-start; justify-content: center;
}
/* desktop: the conversation plays IN a scene (config.conversation.scene, cover-cropped) */
.tab-conv-modal--scene {
  background-size: cover; background-position: center bottom;
  border-color: var(--white-a35);
}
/* staged mode (config.conversation.staging): the modal IS a mini narrative stage —
   locked to the 1920x821 slide aspect so the staged screen's deck percentages
   (characterSlots, bubble boxes) land exactly where the deck put them, and the
   scene plate fits with no crop. */
.tab-conv-modal--stage {
  aspect-ratio: var(--chrome-stage-ratio);
  width: min(1500px, 92%, calc(88vh * 2.338));
  height: auto;
}
.tab-conv-slot { position: absolute; }
/* canvas aspect equals the slot aspect (registration canvases, §30.18) — a plain
   100% fill is pixel-exact, no object-fit. Every pose the character wears is
   STACKED in the slot (absolute, inset 0) and cross-faded by clock-driven inline
   opacity, the §30.18 rule: a swap changes only opacity, never geometry. */
.tab-conv-slot img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.tab-conv-shell { position: absolute; z-index: var(--z-l2); container-type: size; }
.tab-conv-shell > img { width: 100%; height: 100%; display: block; }
.tab-conv-shelltext {
  position: absolute; padding: var(--space-p10); overflow: hidden;
  display: flex; align-items: center; justify-content: center; text-align: center;
  font-family: var(--font-ui);
  font-weight: var(--fw-semibold); line-height: var(--lh-snug); color: var(--ink);
}
.tab-conv-shelltext p { margin: 0; }
.tab-conv-char {
  position: absolute; bottom: 0;
  height: 72%; width: auto;
  filter: drop-shadow(0 10px 24px var(--black-a35));
}
.tab-conv-char--left  { left: clamp(16px, 6%, 70px); }
.tab-conv-char--right { right: clamp(16px, 6%, 70px); }
/* flip on the IMG itself (no animated container here), for staging that needs a
   figure mirrored to face its scene partner */
.tab-conv-char.flip { transform: scaleX(-1); }
/* the bubble sits ABOVE the speaker and wears the narrative-scene bubble shape:
   white fill, dark-navy outline, 15px radius, layered wedge tail pointing down
   at the speaker's head (same double-triangle as .ppt-comic-bubble). */
.tab-conv-bubble {
  position: absolute; top: 6%;
  max-width: min(520px, 46%);
  background: var(--surface-card); color: var(--ink);
  border: var(--border-comic) solid var(--bar-deep); border-radius: var(--radius-r15);
  padding: var(--space-p14) var(--space-p18);
  box-shadow: 0 6px 16px var(--ink-deep-a35);
  animation: mvModalIn var(--dur-t200) var(--ease-standard);
}
.tab-conv-bubble.is-left  { left: clamp(80px, 10%, 150px); }
.tab-conv-bubble.is-right { right: clamp(80px, 10%, 150px); }
.tab-conv-bubble::after {
  content: ""; position: absolute; top: 100%; width: 0; height: 0;
  border: 14px solid transparent; border-top-color: var(--bar-deep);
}
.tab-conv-bubble::before {
  content: ""; position: absolute; top: calc(100% - 5px); width: 0; height: 0;
  border: 10px solid transparent; border-top-color: var(--surface-card); z-index: var(--z-l1);
}
.tab-conv-bubble.is-left::after   { left: var(--space-p44); }
.tab-conv-bubble.is-left::before  { left: var(--space-p48); }
.tab-conv-bubble.is-right::after  { right: var(--space-p44); }
.tab-conv-bubble.is-right::before { right: var(--space-p48); }
.tab-conv-speaker {
  display: block;
  font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui);
  letter-spacing: var(--track-speaker); text-transform: uppercase;
  color: var(--signal);
  margin-bottom: var(--space-p8);
}
.tab-conv-text { margin: 0; font: var(--fw-regular) var(--type-body-lg)/var(--lh-prose) var(--font-ui); }
.tab-conv-btn {
  position: absolute; bottom: var(--space-p26); left: 50%; transform: translateX(-50%);
  border: none; border-radius: var(--radius-pill); padding: var(--space-p15) var(--space-p40); cursor: pointer;
  font: var(--fw-extrabold) var(--type-body)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-dialogue);
  background: var(--signal); color: var(--text-on-signal);
}
.tab-conv-btn:hover { background: var(--signal-hover); }
.tab-conv-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--signal-a55); }
@media (max-width: 1024px /* bp:tablet */) {
  /* tablet/phone: the responsive player scrolls the DOCUMENT, so an absolute
     scrim can sit outside the viewport — pin it to the viewport instead. The
     modal body is the comic talking-head chat (.ppt-comic reused from the
     narrative scenes), not the full figures. */
  .tab-conv-scrim { position: fixed; }
  /* The scrim is the whole viewport, so centring in it ignored the player's chrome: the modal ran
     to y=601 on a 360x646 phone while the bottom bar starts at 550, and the finish button — which
     is `position: absolute; bottom: 18px` against the modal — sat at 536-582, half of it behind the
     bar (author-reported 2026-08-08, measured at that exact size). Inset the scrim by the chrome so
     the modal centres in the CONTENT zone, and cap its height to that zone.
     -20px leaves a visible margin rather than butting the modal against both bars. */
  .tab-conv-scrim { padding: var(--rc-header-h, var(--chrome-flow-header-h)) 0 var(--chrome-flow-footer-h); box-sizing: border-box; }
  .tab-conv-modal {
    width: 94%;
    height: min(560px, calc(100dvh - var(--rc-header-h, var(--chrome-flow-header-h)) - var(--chrome-flow-footer-h) - 20px));
  }
  /* on the comic's signal-blue halftone the signal-blue pill disappears — go
     dark navy with white text (author 2026-07-24), same ink as the comic lines */
  .tab-conv-btn { bottom: var(--space-p18); background: var(--bar-deep); color: var(--text-on-navy); }
  .tab-conv-btn:hover { background: var(--surface-chrome); }
}
/* comic body fills the modal; the button floats over its lower edge, so the
   halftone panel keeps clearance below the names */
.tab-conv-comic {
  position: absolute; inset: 0;
  flex: none; border-bottom: none; border-radius: inherit;
  padding: 1.4vh 5vw var(--space-p84);
  font-family: var(--font-ui);
}
/* the modal is narrower than the viewport the comic was tuned for — pin each
   bubble inside its own side instead of centering it over the head, so a long
   line can never overflow the modal's clipped edge */
.tab-conv-comic .ppt-comic-bubble { width: min(58vw, 420px); max-width: none; }
.tab-conv-comic .ppt-comic-side:first-child .ppt-comic-bubble { left: 0; transform: none; }
.tab-conv-comic .ppt-comic-side:last-child .ppt-comic-bubble  { left: auto; right: 0; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .tab-conv-scrim, .tab-conv-bubble { animation: none; }
}

/* ============================================================================
   Carousel-gated entry modals (CarouselGate, templates/base.js): a modal held
   back during the slide transition mounts AFTERWARD with this class — it fades
   in, in place; the direct child (the modal card) skips its pop entrance so
   nothing jumps. Deeper animations (bubbles, reveals) are untouched. */
.mv-modal-fade-in { animation: mvFadeInSlow var(--dur-t750) var(--ease-default) both; }   /* author-tuned 2026-07-26: a gradual arrival after the slide seats */
/* While a gated modal is pending (slide still riding in), this transparent layer
   covers the screen content so nothing beneath can be clicked or hover-sounded;
   the modal's own scrim takes over once it mounts. Covers the LAYER, not the
   chrome — navigation stays live. */
.mv-modal-pending-blocker { position: absolute; inset: 0; z-index: var(--z-pending-blocker); }

/* Timeline conversation — the pending Sue line as a SILENT-BUTTON bubble (click =
   say it; the clock holds at her line until then). It keeps the bubble's look;
   the affordance is the pointer, a signal ring, and the hover lift. */
button.tab-conv-say { cursor: pointer; font: inherit; text-align: inherit; color: inherit; }
/* × close on the conversation modal — REPLAY passes only (bail out of a rewatch) */
.tab-conv-close {
  position: absolute; top: var(--space-p10); right: var(--space-p12); z-index: var(--z-l5);
  width: var(--size-p40); height: var(--size-p40); border: 0; border-radius: var(--radius-circle);
  background: var(--bar-deep-a55); color: var(--text-on-navy); cursor: pointer;
  font: var(--fw-regular) var(--type-h2)/var(--lh-solid) system-ui, sans-serif;
}
.tab-conv-close:hover { background: var(--bar-deep-a80); }
.tab-conv-close:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
/* THE LEARNER'S LINE PULSES (author 2026-07-31). It is the one bubble on screen that is a
   BUTTON, and a static glow read as decoration — the conversation just stopped and nothing said
   the learner was being waited on. The same `cvoPulse` the Conversation Decision uses for its
   choice bubbles (screens.css), so the two interactions signal "click me" identically. */
button.tab-conv-bubble.tab-conv-say,
button.ppt-comic-bubble.tab-conv-say {
  border: var(--border-w2) solid var(--signal);
  animation: convSayPulse var(--dur-pulse) var(--ease-in-out) infinite;
}
button.tab-conv-shell.tab-conv-say { background: transparent; border: 0; padding: 0; position: absolute; }
button.tab-conv-shell.tab-conv-say img { animation: cvoPulse var(--dur-pulse) var(--ease-in-out) infinite; }

/* The shell is a transparent PNG, so its glow has to be a drop-shadow on the artwork (cvoPulse).
   The comic/tablet bubbles are real boxes, so theirs is a box-shadow — same rhythm, same colour. */
@keyframes convSayPulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--signal-a20); }
  50%      { box-shadow: 0 0 0 7px var(--signal-a45); }
}
@media (prefers-reduced-motion: reduce) {
  button.tab-conv-bubble.tab-conv-say,
  button.ppt-comic-bubble.tab-conv-say { animation: none; box-shadow: 0 0 0 4px var(--signal-a35); }
  button.tab-conv-shell.tab-conv-say img { animation: none; filter: drop-shadow(0 0 5px var(--signal-a70)); }
}

/* A finished line fades rather than vanishing — see the `visible` filter in tabs.js, which drops
   earlier bubbles the moment the floor passes to the learner. Matches CONV_FADE_MS. */
.tab-conv-shell { transition: opacity var(--dur-t300) var(--ease-default); }
@media (prefers-reduced-motion: reduce) { .tab-conv-shell { transition: none; } }
.tab-conv-say:hover { filter: brightness(1.05); }
.tab-conv-say:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.mv-modal-fade-in > * { animation: none; }

/* ============================================================================
   Pressed (:active) states — QoL pass. The universal button press (brightness,
   player.css) covers everything; these add the physical push for the pill CTAs
   and a compress for small icon/close buttons. Kept out of @media (hover:hover)
   deliberately: press feedback belongs on touch too. */
/* THE PRESS MUST NOT UNDO THE CENTRING (author-reported 2026-07-31). The speech-modal's
   button is centred with `transform: translateX(-50%)`, and the shared :active rule below sets
   `transform: translateY(1px)` — which REPLACES the transform rather than adding to it, so the
   button snapped right by half its width the instant it was pressed and slid out from under the
   cursor, and the click never landed. The author's own clue named the mechanism: pressing the
   RIGHT edge worked more often, because after the jump the pointer was still over the button.
   Re-declare both parts together. Any future centred button needs the same treatment. */
.mc-intro-modal--speech .mc-intro-btn:not(:disabled):active {
  transform: translateX(-50%) translateY(1px);
}

.cmp-btn:not(:disabled):active, .mc-intro-btn:not(:disabled):active,
.tpl-submit.is-shown:not(:disabled):active, .tpl-fb-btn:active {
  background: var(--signal-pressed); transform: translateY(1px);
}
/* .tab-conv-btn is centered via `left:50%; transform:translateX(-50%)` (its own
   rule, above) — sharing the generic :active rule REPLACED that transform outright
   (CSS transform doesn't merge partial values), so the button lost its -50% and
   snapped to the LEFT EDGE the instant it was pressed — visually "teleporting"
   right by about half its own width the moment a finger/mouse touched down. Any
   real tap that didn't move would then release OUTSIDE the button's new bounds,
   missing the click — exactly the "doesn't work, takes a few clicks" symptom
   paired with "teleports right" (author bug report, 2026-07-27). Fixed with its
   own :active rule that composes both transforms instead of one replacing the
   other. */
.tab-conv-btn:active {
  background: var(--signal-pressed);
  transform: translateX(-50%) translateY(1px);
}
/* THIRD INSTANCE OF THE SAME BUG (author-reported 2026-08-05, scenario-3-verify-reading).
   `.mc-cards-arrow` is vertically centred with `transform: translateY(-50%)`, and this press
   rule used to say `translateY(1px) scale(0.94)` — which REPLACES that transform rather than
   adding to it, so the arrow dropped by half its own height (22px of 44px) the instant it was
   pressed, slid out from under the pointer, and the click released outside its new bounds. The
   author's report names the mechanism exactly: "when you click on the top eighty percent of the
   button… the button does nothing… but if you click on the bottom twenty to thirty percent, the
   button actually moves." That bottom strip is the only part still under the arrow after it
   jumps. Re-declare the centring alongside the press, so the drop is the intended 1px. */
.mc-cards-arrow:not(:disabled):active { transform: translateY(calc(-50% + 1px)) scale(0.94); }
.gs-reveal-close:active, .scn-callout-close:active { transform: scale(0.88); }

/* Dialog containers are given tabIndex="-1" and focused programmatically when they open
   (modal-focus.js), so the browser draws a focus ring around the whole card. That ring is
   noise, not an affordance: a -1 element cannot be reached by Tab, so the ring never marks
   "you are here" for a keyboard user. The real controls INSIDE keep their :focus-visible
   rings, which is what actually guides keyboard navigation. Suppressing only :focus on the
   container leaves those untouched. */
.tpl-fb:focus,
.mc-intro-modal:focus,
.scn-callout-card:focus { outline: none; }

/* PRACTICE REVIEW (author 2026-07-29) — formative scenario results.
   Visually a sibling of the graded review and reuses its row styling, because a result
   reads back the same either way. What it deliberately does NOT get is a score, a
   percentage or a pass badge: it must never be mistakable for a grade. The note under
   the heading says so in words rather than relying on the styling to imply it. */
.sc-review--practice {
  margin-top: var(--space-p22); padding-top: var(--space-p22);
  border-top: var(--border-w1-5) solid var(--white-a16);
}
.sc-review--practice .sc-review-title { margin-bottom: var(--space-p6); }
.sc-review-note {
  font: var(--fw-medium) var(--type-caption)/var(--lh-prose) var(--font-ui);
  color: var(--white-a66); margin-bottom: var(--space-p16);
}
/* Awaiting-quiz state: the message sits above the practice review rather than replacing
   the page, so a learner who has only done practice still sees their own work. */
/* `.sc` is a flex ROW with align-items:flex-start; switching to a column without also
   resetting alignment made every child shrink-wrap to its content (measured: the practice
   block came out 351px wide in a 1440px viewport, hugging the left edge). */
.sc-awaiting { display: flex; flex-direction: column; align-items: stretch; }
/* Keep the message exactly as it was before the practice review existed: same 520px
   measure, same centring. Only the surrounding container changed, and a learner who has
   done nothing yet should see no difference at all. */
.sc-awaiting .sc-empty { margin: 0 auto var(--space-p4); max-width: var(--size-p520); }
.sc-awaiting .sc-review--practice { border-top: none; margin-top: var(--space-p8); }

/* Attempt/remediation line — PRACTICE review only. Quiet by default; the remediated
   badge is the one that earns colour, because "got there after feedback" is the finding
   a formative review exists to report. */
.sc-q-effort {
  display: flex; align-items: center; gap: var(--space-p8); margin-top: var(--space-p4);
  font: var(--fw-semibold) var(--type-label)/var(--lh-text-tight) var(--font-ui);
  color: var(--muted);
}
.sc-q-remediated {
  padding: var(--space-p2) var(--space-p8); border-radius: var(--radius-pill);
  background: var(--signal-a12); color: var(--signal);
  font: var(--fw-extrabold) var(--type-nano)/var(--lh-prose-loose) var(--font-ui);
  letter-spacing: var(--track-caption); text-transform: uppercase;
}

/* ===== Swipe Classification — one card at a time, classified left or right ==============
   THE LAYOUT IS WIDTH-DRIVEN THROUGHOUT AND NEVER USES VIEWPORT HEIGHT. `.swc-media` carries
   the aspect-ratio and the img fills it absolutely, so the picture's height is always a
   function of its width — which is what keeps the apparent zoom identical on desktop, tablet
   and phone. No `vh`, no `dvh`, no fixed pixel heights anywhere in this block: those are what
   collapse a frame's ratio on a portrait screen and produce the 200%-zoom look.
   The card is capped rather than fluid-to-infinity so the whole interaction (title,
   instruction, progress, card, buttons) still fits the non-scrolling 1080 desktop canvas. ==== */
.tpl-page--swipe { display: flex; align-items: flex-start; justify-content: center; padding: var(--space-p18) var(--space-p16) var(--space-p24); }
.swc { width: 100%; max-width: var(--size-p720); display: flex; flex-direction: column; align-items: center; gap: var(--space-p8); }
/* LIGHT TEXT: the header sits on the NAVY STAGE, not on the white card. Authored dark first
   (copied from the card-based templates) and it measured 1.1:1 against the stage — effectively
   invisible, caught on the first render. Only the card's own prompt/context stay dark, because
   those really are on white. */
.swc-title {
  margin: 0; text-align: center;
  font: var(--fw-extrabold) var(--type-h2)/var(--lh-title) var(--font-head); color: var(--text-on-navy);
}
.swc-instr {
  margin: 0; text-align: center; max-width: var(--size-p620);
  font: var(--fw-regular) var(--type-body)/var(--lh-text-loose) var(--font-ui);
  color: var(--white-a92);
}
.swc-progress {
  margin: var(--space-p2) 0 0;
  font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-head); letter-spacing: var(--track-badge);
  text-transform: uppercase; color: var(--signal);
}

/* the card + its two directional cues, side by side on wide screens */
.swc-stage { display: flex; align-items: center; justify-content: center; gap: var(--space-p14); width: 100%; }
.swc-cue {
  flex: 0 0 auto; opacity: var(--alpha-25); transition: opacity var(--dur-t160) var(--ease-default);
  font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-head); letter-spacing: var(--track-badge);
  text-transform: uppercase; color: var(--text-heading);
  writing-mode: vertical-rl;
}
.swc-cue--left { transform: rotate(180deg); }
.swc-cue.is-on { opacity: 1; color: var(--signal); }

.swc-card {
  flex: 1 1 auto; max-width: var(--size-p560); box-sizing: border-box;
  background: var(--surface-card); border: var(--border-hairline) solid var(--border); border-radius: var(--radius-card);
  padding: var(--space-p14) var(--space-p14) var(--space-p12); box-shadow: 0 10px 26px var(--navy-a14);
  /* VERTICAL PANNING STAYS WITH THE BROWSER. This is the CSS half of the swipe contract: the
     page keeps scrolling natively and the renderer only ever sees horizontal intent, so a
     swipe UI on a long page cannot trap the learner. */
  touch-action: pan-y;
  /* Text selection and native image dragging both hijack a horizontal drag — the drag steals
     the pointer sequence outright (pointercancel instead of pointerup). */
  user-select: none; -webkit-user-select: none; -webkit-user-drag: none;
  cursor: grab; transition: transform var(--dur-base) var(--ease-standard);
}
.swc-card.is-dragging { cursor: grabbing; }
.swc-card:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 3px; }

.swc-media {
  position: relative; width: 100%; aspect-ratio: 4 / 3;   /* inline override wins if authored */
  overflow: hidden; border-radius: var(--radius-button); background: var(--page);
  border: var(--border-hairline) solid var(--border);
}
.swc-img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.swc-prompt {
  margin: var(--space-p12) var(--space-p2) 0;
  font: var(--fw-bold) var(--type-copy)/var(--lh-text) var(--font-ui); color: var(--ink);
}
.swc-context { margin: var(--space-p6) var(--space-p2) 0; font: var(--fw-regular) var(--type-small)/var(--lh-text-loose) var(--font-ui); color: var(--secondary); }
/* HOW TO ANSWER — a top-level instruction line, not a caption inside the card (2026-08-05).
   It sits on the navy stage now rather than on the white card face, so `--muted` grey would be
   both illegible and the wrong signal. The lozenge is what makes it "readily visible" without
   competing with the title: same signal blue the progress line uses, at instruction size. */
.swc-hint {
  margin: var(--space-p4) 0 0; padding: var(--space-p7) var(--space-p18); max-width: var(--size-p620); text-align: center;
  border-radius: var(--radius-pill);
  background: var(--signal-a16);
  border: var(--border-hairline) solid var(--signal-a45);
  font: var(--fw-extrabold) var(--type-micro)/var(--lh-bubble) var(--font-head); letter-spacing: var(--track-hint);
  text-transform: uppercase; color: var(--text-on-navy);
}

/* Both controls, always, on every platform. 44px minimum touch target per WCAG 2.5.5. */
.swc-controls { display: flex; gap: var(--space-p12); justify-content: center; width: 100%; max-width: var(--size-p560); margin-top: var(--space-p12); }
.swc-btn {
  flex: 1 1 0; min-height: var(--size-p48); min-width: var(--hit-target); padding: var(--space-p12) var(--space-p18);
  border-radius: var(--radius-pill); border: var(--border-w2) solid var(--text-heading);
  background: var(--surface-card); color: var(--text-heading); cursor: pointer;
  font: var(--fw-extrabold) var(--type-body)/var(--lh-compact) var(--font-ui);
}
.swc-btn--right { background: var(--surface-chrome); color: var(--text-on-navy); }
.swc-btn:hover:not(:disabled) { border-color: var(--signal); }
.swc-btn:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.swc-btn:disabled { opacity: var(--alpha-45); cursor: default; }

/* --- the card is TWO-SIDED: feedback is its REVERSE (author 2026-07-30) ------------------
   The feedback used to be a panel BELOW the card. On a narrow window that put it under the
   fold, so answering meant scrolling down to read it and back up for the next picture, seven
   times over. Flipping the card keeps the answer and its explanation in the same place, and
   the Next button rides on the back — nothing below the card to reach for.

   Same 3D mechanic as .fc-card (Flip Cards) and .mc-card-flip (the Multiple Choice wrong-answer
   flip), so the gesture reads identically everywhere it is used.

   THE FRONT STAYS IN NORMAL FLOW and is what gives the card its height; the back is absolutely
   positioned over it. Neither face needs a fixed size, so a long prompt or a tall image simply
   makes the card taller and the back follows. */
.swc-card { perspective: 1400px; }
.swc-card-inner {
  position: relative; transform-style: preserve-3d;
  transition: transform var(--dur-flip-delay) var(--ease-b-3-7-3-1);
}
.swc-card.is-flipped .swc-card-inner { transform: rotateY(180deg); }
.swc-face { backface-visibility: hidden; -webkit-backface-visibility: hidden; }
/* WHICHEVER FACE IS SHOWING IS THE ONE IN FLOW, so the card is only ever as tall as the side you
   can actually see. The front is tall because of the picture; the back is a couple of lines and a
   button. Sizing the card to the front in both states left the flipped card mostly empty AND — on
   a short window — pushed Next below the fold, which is the scrolling the flip exists to remove
   (author, 2026-07-30). Swapping which face is absolute costs nothing and the card visibly
   shrinks as it turns. */
/* THE CARD KEEPS ITS SIZE WHEN IT FLIPS (author 2026-07-30). The front is always the face in
   flow, so Hold and Proceed sit at exactly the same place whichever side is showing — a control
   that moves under the cursor between "answer" and "next" is worse than one that is a little
   further down. (An earlier pass shrank the card to the feedback; the author saw it and asked
   for the stable size instead.) */
.swc-face--front { position: relative; }
.swc-face--back {
  position: absolute; inset: 0; transform: rotateY(180deg);
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  gap: var(--space-p12); border-radius: var(--radius-option); padding: var(--space-p18) var(--space-p18) var(--space-p72); text-align: center;
}
/* Next / Try again is CENTRED on the card and pinned near its bottom edge, so it lands directly
   above Hold and Proceed — one vertical line of controls, and the eye never hunts for it. The
   bottom padding above reserves its row so a long explanation can never run underneath it. */
.swc-fb-btn {
  position: absolute; left: 50%; bottom: var(--space-p18); transform: translateX(-50%);
}
/* Meaning is carried by the TITLE TEXT, not by the colour — the tint is reinforcement only. */
.swc-face--back.is-correct { background: var(--correct-a10); box-shadow: inset 0 0 0 2px var(--correct); }
.swc-face--back.is-wrong { background: var(--incorrect-a09); box-shadow: inset 0 0 0 2px var(--incorrect); }
/* A long explanation scrolls INSIDE the face rather than overflowing the card — the whole point
   of the flip is that nothing moves off-screen. */
.swc-fb-scroll { flex: 0 1 auto; overflow-y: auto; min-height: 0; }
.swc-fb-scroll:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; border-radius: var(--radius-sm); }
@media (prefers-reduced-motion: reduce) { .swc-card-inner { transition: none; } }

.swc-fb-title { font: var(--fw-extrabold) var(--type-small)/var(--lh-title) var(--font-head); color: var(--text-heading); }
.swc-fb-body { margin: var(--space-p6) 0 0; font: var(--fw-regular) var(--type-small)/var(--lh-prose) var(--font-ui); color: var(--ink); }
.swc-fb-btn {
  min-height: var(--hit-target); padding: var(--space-p10) var(--space-p22); border: none; cursor: pointer;
  border-radius: var(--radius-pill);
  background: var(--signal); color: var(--text-on-signal);
  font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui);
}
.swc-fb-btn:focus-visible { outline: var(--border-focus) solid var(--text-heading); outline-offset: 2px; }

/* THE COMPLETION PANEL NEEDS A SURFACE (author 2026-07-30). Its title is `--navy` and its body
   is `--ink` — both dark — and it was rendering with NO background, straight onto the navy stage:
   #132554 on #1A2E60 is about 1.1:1, i.e. invisible. Every other state in this template sits on
   the white card; this one was the exception, and the missing surface and the unreadable text are
   the same bug. Matches .swc-card exactly (white, same radius, border and shadow, same 560px
   ceiling) so the finish reads as the last card in the set rather than a different screen. */
.swc-done {
  text-align: center; padding: var(--space-p32) var(--space-p24);
  width: 100%; max-width: var(--size-p560); margin: 0 auto; box-sizing: border-box;
  background: var(--surface-card); border: var(--border-hairline) solid var(--border); border-radius: var(--radius-card);
  box-shadow: 0 10px 26px var(--navy-a14);
}
.swc-done-mark {
  width: var(--size-p56); height: var(--size-p56); margin: 0 auto var(--space-p12); border-radius: var(--radius-circle);
  background: var(--correct); color: var(--text-on-accent);
  font: var(--fw-extrabold) var(--type-h1)/var(--lh-done-mark) var(--font-head);
}
.swc-done-title { font: var(--fw-extrabold) var(--type-lead)/var(--lh-title) var(--font-head); color: var(--text-heading); }
.swc-done-body { margin: var(--space-p8) 0 0; font: var(--fw-regular) var(--type-body)/var(--lh-prose) var(--font-ui); color: var(--ink); }

@media (max-width: 1024px /* bp:tablet */) {
  /* The cues are horizontal-only furniture; on narrow screens the buttons carry the labels. */
  .swc-cue { display: none; }
  .swc-card { max-width: none; }
  .swc { max-width: var(--size-p560); }
}
@media (max-width: 600px /* bp:phone */) {
  .tpl-page--swipe { padding: var(--space-p14) var(--space-p14) var(--space-p20); }
  .swc-title { font-size: var(--type-h4); }
  /* Body text does NOT shrink below the course's normal size on phone. */
  .swc-instr { font-size: var(--type-body); }
  .swc-prompt { font-size: var(--type-copy); }
  .swc-context { font-size: var(--type-small); }
  /* HOLD AND PROCEED STAY SIDE BY SIDE, ON EVERY WIDTH (author 2026-07-31). They used to stack
     into a column here. Two reasons that was wrong: the pair is a LEFT/RIGHT decision — the
     buttons mirror "swipe left to hold, swipe right to proceed", and stacking them into
     top/bottom breaks the spatial mapping the instruction line just taught. And it made phone
     the odd one out against tablet and desktop. They fit: `flex: 1 1 0` on a 44px minimum still
     clears the WCAG target at 320px. */
  .swc-btn { padding: var(--space-p12) var(--space-p10); font-size: var(--type-small); }   /* narrower gutters so both labels fit */
}
@media (prefers-reduced-motion: reduce) {
  .swc-card { transition: none; }
  .swc-cue { transition: none; }
}

/* DEV-ONLY modal escape hatch (intro-narration.js `devClose`). It never had any CSS, so once the
   narration modal became a grid it stretched into a full-width white strip under the speech box.
   TOP-LEFT by the author's call (2026-08-01): only he ever sees it, the left corner is clear of
   the speech box, and presentation does not matter for a control no learner can reach. Double
   gated in JS by DEV_MODAL_CLOSE && ctx.dev — REMOVE BEFORE SHIPPING with the DEV pill. */
.mv-dev-modal-close {
  position: absolute; top: var(--space-p6); left: var(--space-p8); z-index: var(--z-l5);
  width: var(--size-p22); height: var(--size-p22); padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: var(--border-hairline) solid var(--white-a35); border-radius: var(--radius-r4);
  background: var(--black-a28); color: var(--text-on-navy);
  font: var(--fw-semibold) var(--type-small)/var(--lh-solid) var(--font-ui);
  cursor: pointer; opacity: var(--alpha-55);
}
.mv-dev-modal-close:hover { opacity: 1; }
.mv-dev-modal-close:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--signal-a60); }

/* ===== Video — a clip on the stage, driven by the chrome's transport (2026-09-14) ====
   The stage is a letterboxed frame at the clip's aspect (--vid-ar, from config.aspect),
   sized to the room left by the head so the desktop composition fits with NO scroll; the
   caption track is the template pages' shared fixed one (screens.css .ppt-captions--track),
   untouched. Phone: the frame takes the width and the page scrolls. Tokens only. */
.tpl-page--video { display: flex; flex-direction: column; min-height: 100%; container-type: size; }
.vid-page { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-p14); }
.vid-head { text-align: center; flex-shrink: 0; }
.vid-eyebrow { font: var(--fw-extrabold) var(--type-vid-eyebrow)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-cover-eyebrow); text-transform: uppercase; color: var(--signal); }
.vid-title { margin: var(--space-p6) 0 0; font: var(--fw-extrabold) var(--type-vid-title)/var(--lh-heading) var(--font-ui); letter-spacing: var(--track-heading); color: var(--text-on-navy); }
.vid-frame {
  position: relative; flex: 0 1 auto; min-height: 0;
  aspect-ratio: var(--vid-ar, 16 / 9);
  width: min(100%, calc((100cqh - 130px) * var(--vid-ar, 16 / 9)));
  max-height: 100%;
}
.vid-stage {
  width: 100%; height: 100%;
  border-radius: var(--radius-option); overflow: hidden;
  background: var(--surface-chrome); box-shadow: 0 8px 22px var(--black-a45);
}
.vid-el { display: block; width: 100%; height: 100%; object-fit: contain; background: var(--surface-chrome); }
.vid-caption { margin: var(--space-p6) 0 0; text-align: center; font: var(--fw-medium) var(--type-vid-caption)/var(--lh-text) var(--font-ui); color: var(--text-on-navy-soft); }
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--video { container-type: normal; }
  .vid-frame { width: 100%; max-height: none; }
}

/* ============================================================================
   capturedControls (2026-09-15) - a captured walk that ends on a control the learner operates
   (renderers/templates/captured-controls.js). ONE picture end to end: the clip plays like the
   video template's, the control's captured strip fades in over the clip's last frame (frame 0
   IS that frame), the grip is a slider the learner drags, clicks or steps, and a pop-up note in
   the theme's colours carries the instruction, then the done line. Every value reads a token;
   the classic set is untouched (new prefix, no shared rule). */
.tpl-page--captured { display: flex; flex-direction: column; min-height: 100%; container-type: size; }
.cc-page { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-p14); }
.cc-head { text-align: center; flex-shrink: 0; }
.cc-eyebrow { font: var(--fw-extrabold) var(--type-vid-eyebrow)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-cover-eyebrow); text-transform: uppercase; color: var(--signal); }
.cc-title { margin: var(--space-p6) 0 0; font: var(--fw-extrabold) var(--type-vid-title)/var(--lh-heading) var(--font-ui); letter-spacing: var(--track-heading); color: var(--text-on-navy); }
.cc-frame {
  position: relative; flex: 0 1 auto; min-height: 0;
  aspect-ratio: var(--vid-ar, 16 / 9);
  width: min(100%, calc((100cqh - 130px) * var(--vid-ar, 16 / 9)));   /* the video template's own fit: the head's room above the picture */
  max-height: 100%;
}
.cc-stage { position: relative; width: 100%; height: 100%; border-radius: var(--radius-option); overflow: hidden; background: var(--surface-chrome); box-shadow: var(--shadow-frame); }
/* the row (config.layout "row", 2026-09-16): the note in a left column, the stage at full height beside it at its own
   ratio - the height is the head's room or the width left beside the column, whichever is smaller (softwareSim's rule) */
.cc-frame.is-row { aspect-ratio: auto; width: 100%; max-height: none; display: flex; flex-direction: row; align-items: stretch;
  height: min(calc(100cqh - 130px), calc((100cqw - var(--size-p300)) * 9 / 16)); }
.cc-frame.is-row .cc-note-col { flex: 1 1 var(--size-p300); min-width: var(--size-p300); padding-right: var(--space-p14); }
.cc-frame.is-row .cc-stage { flex: 0 0 auto; width: auto; height: 100%; aspect-ratio: var(--vid-ar, 16 / 9); }
/* the view (2026-09-19): the whole capture inside the stage box; with config.window it is larger than the box and shifted so
   the window fills it (captured-controls.js captureWindow writes the four percentages inline); without, it is the box */
.cc-view { position: absolute; inset: 0; }
.cc-clip { display: block; width: 100%; height: 100%; object-fit: contain; background: var(--surface-chrome); }
/* the strip: every frame decoded and in place; the current one opaque. Hidden until the walk arrives. */
.cc-strip { position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity var(--dur-t260) var(--ease-standard); }
.is-arrived .cc-strip { opacity: 1; }
.cc-still { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; opacity: 0; }
.cc-still.is-current { opacity: 1; }
/* the grip: where the hand goes. A slider - it moves with the frame it is on. */
.cc-grip {
  position: absolute; width: var(--size-p64); height: var(--size-p64); transform: translate(-50%, -50%);
  border-radius: var(--radius-circle); background: var(--signal-a20); cursor: grab; touch-action: none; z-index: var(--z-l2);
}
.cc-grip:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.cc-grip.is-dragging { cursor: grabbing; }
.cc-grip-ring {
  position: absolute; inset: 0; border-radius: var(--radius-circle); border: var(--border-w2) solid var(--signal); opacity: var(--alpha-85);
  animation: ccPulse var(--dur-t600) var(--ease-in-out) infinite alternate;
}
.cc-grip:hover .cc-grip-ring, .cc-grip:focus-visible .cc-grip-ring, .cc-grip.is-armed .cc-grip-ring, .cc-grip.is-dragging .cc-grip-ring { opacity: 1; animation: none; }
.is-done .cc-grip { pointer-events: none; opacity: 0; transition: opacity var(--dur-t300) var(--ease-standard); }
/* a press: one button at the control's centre - the same ring, a pointer cursor, gone once pressed */
.cc-press {
  position: absolute; width: var(--size-p64); height: var(--size-p64); transform: translate(-50%, -50%); padding: 0;
  border-radius: var(--radius-circle); border: 0; background: var(--signal-a20); cursor: pointer; z-index: var(--z-l2);
}
.cc-press:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.cc-press:hover .cc-grip-ring, .cc-press:focus-visible .cc-grip-ring, .cc-press.is-pressing .cc-grip-ring { opacity: 1; animation: none; }
.is-done .cc-press { pointer-events: none; opacity: 0; transition: opacity var(--dur-t300) var(--ease-standard); }
@keyframes ccPulse { from { transform: scale(1); } to { transform: scale(1.12); } }
/* the two-click target: where it should go, shown once the grip is armed */
.cc-target {
  position: absolute; transform: translate(-50%, -50%); width: var(--size-p64); height: var(--size-p64); padding: 0;
  border-radius: var(--radius-circle); border: var(--border-w2) dashed var(--signal); background: var(--signal-a12); color: var(--text-on-accent);
  cursor: pointer; z-index: var(--z-l2); display: grid; place-items: center;
  font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-label); text-transform: uppercase;
}
.cc-target:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.cc-target-ring { position: absolute; inset: 0; border-radius: var(--radius-circle); border: var(--border-w2) solid var(--signal-a55); animation: ccPulse var(--dur-t600) var(--ease-in-out) infinite alternate; }
.cc-target-label { position: relative; }
/* a pause point's callout (2026-09-16): the same ring as the two-click target, on the thing the note points at;
   the pause's note shows while the clip is held (.is-held), like the instruction shows once arrived */
.cc-callout {
  position: absolute; transform: translate(-50%, -50%); width: var(--size-p64); height: var(--size-p64); padding: 0;
  border-radius: var(--radius-circle); border: var(--border-w2) dashed var(--signal); background: var(--signal-a12); color: var(--text-on-accent);
  cursor: pointer; z-index: var(--z-l2); display: grid; place-items: center;
  font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-label); text-transform: uppercase;
}
.cc-callout:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.is-held .pn-note.is-pause { opacity: 1; transform: none; }
.pn-note.is-lead.is-aside + .pn-note.is-pause { margin-top: var(--space-p12); }   /* under a kept lead note (2026-09-19) */
/* the pop-up note (pop-note.js; shared with softwareSim since 2026-09-16 - it was the captured block's own
   note): the theme's card on the picture - in Precision Tork's theme, their colours. Hidden until the
   scene's .is-arrived. */
.pn-note {
  position: absolute; top: var(--space-p16); left: var(--space-p16); max-width: var(--size-p420); z-index: var(--z-l3);
  background: var(--surface-card); color: var(--text-body); border-left: var(--border-w4) solid var(--signal); border-radius: var(--radius-card);
  padding: var(--space-p14) var(--space-p18); box-shadow: var(--shadow-nav);
  opacity: 0; transform: translateY(var(--space-p8)); transition: opacity var(--dur-t300) var(--ease-standard), transform var(--dur-t300) var(--ease-standard);
}
.is-arrived .pn-note { opacity: 1; transform: none; }
.pn-note.is-lead { opacity: 1; transform: none; }   /* the note for the walk shows before arrival (2026-09-19) */
.pn-strong { font-weight: var(--fw-extrabold); }   /* a body's **bold** span (2026-09-19) */
.pn-under { text-decoration: underline; text-underline-offset: var(--space-p2); }   /* a body's __underlined__ span (2026-09-19) */
/* THE NEXT STEP'S BOX (2026-09-19): a second card in the signal colour under the done note - or its last block when the
   note pops over a stage, and under the pad's done line */
.pn-next { position: static; margin-top: var(--space-p12); max-width: var(--size-p420); box-sizing: border-box; padding: var(--space-p12) var(--space-p18); background: var(--signal); color: var(--text-on-signal); border-radius: var(--radius-card); box-shadow: var(--shadow-nav); }
.pn-next.is-inner { max-width: none; margin: var(--space-p12) 0 0; box-shadow: none; }
.pn-next-eyebrow { font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text-on-signal); }
.pn-next-body { margin: var(--space-p4) 0 0; font: var(--fw-bold) var(--type-body)/var(--lh-text) var(--font-ui); color: var(--text-on-signal); }
.pn-note.is-aside + .pn-next { max-width: none; }
.pn-note.is-lead.is-aside + .pn-note { margin-top: var(--space-p12); }   /* the kept lead note, the instruction under it (2026-09-19) */
.pn-eyebrow { font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--signal-text); }
.pn-title { margin: var(--space-p4) 0 0; font: var(--fw-extrabold) var(--type-h3)/var(--lh-heading) var(--font-ui); color: var(--text-heading); }
.pn-body { margin: var(--space-p6) 0 0; font: var(--fw-medium) var(--type-body)/var(--lh-text) var(--font-ui); color: var(--text-prose); }
.pn-keys { margin: var(--space-p8) 0 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-secondary); }
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--captured { container-type: normal; }
  .cc-frame { width: 100%; max-height: none; }
  .cc-frame.is-row { height: auto; flex-direction: column; }
  .cc-frame.is-row .cc-note-col { flex: none; padding-right: 0; margin-bottom: var(--space-p12); }
  .cc-frame.is-row .cc-stage { width: 100%; height: auto; }
  .pn-note { position: static; max-width: none; margin: var(--space-p12) 0 0; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .cc-grip-ring, .cc-target-ring { animation: none; }
}

/* ============================================================================
   softwareSim (2026-09-16) - a machine's own software, drawn in HTML by the course, with the
   learner's press on it (renderers/templates/software-sim.js). The page is COURSE MEDIA mounted
   in a shadow root inside .ss-panel - its stylesheet never touches this one; this block only
   places the row, the panes and the panel's fit, all in tokens. The pop-up note is .pn-* (shared).
   THE ROW (Tyler, 2026-09-16, later): the instructions in the left margin, the live view (3 : 4) in the
   middle, the page as large as the stage allows on the right at its own ratio (5 : 4 for the Touch Panel
   PC). The row's height is the page's room or the width left for two panes beside the note column,
   whichever is smaller - so every softwareSim screen has the same sizes and nothing moves when the
   note's text changes (the note used to sit under the stage and its height resized everything). */
.tpl-page--sim { display: flex; flex-direction: column; min-height: 100%; container-type: size; }
.ss-page { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-p14); }
.ss-head { text-align: center; flex-shrink: 0; }
.ss-eyebrow { font: var(--fw-extrabold) var(--type-vid-eyebrow)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-cover-eyebrow); text-transform: uppercase; color: var(--signal); }
.ss-title { margin: var(--space-p6) 0 0; font: var(--fw-extrabold) var(--type-vid-title)/var(--lh-heading) var(--font-ui); letter-spacing: var(--track-heading); color: var(--text-on-navy); }
.ss-frame {
  position: relative; flex: 0 0 auto; width: 100%;
  /* the row's height: the page's room under the head, or the width the two panes can have beside the note
     column (3/4 + 5/4 = 2 heights), whichever is smaller */
  height: min(calc(100cqh - 92px), calc((100cqw - var(--size-p300)) / 2));   /* 92: the head (one or two lines) and the gap */
}
.ss-stage { position: relative; width: 100%; height: 100%; display: flex; flex-direction: row; align-items: stretch; }
.ss-note-col { flex: 1 1 var(--size-p300); min-width: var(--size-p300); padding-right: var(--space-p14); }
/* the lead-in clip (2026-09-16 - the zoom from where the last walk ended onto the touch screen): over the whole
   row while it plays, ending with the screen where the panel sits; on arrival the panel fades in over it and the
   clip fades out, uncovering the note column and the live pane. Rendered at the row's own aspect (2.61 : 1 -
   1880 x 720), so `contain` leaves no bars */
.ss-clip { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: var(--surface-chrome); transition: opacity var(--dur-t260) var(--ease-standard); }
.is-arrived .ss-clip { opacity: 0; pointer-events: none; }
.ss-live { position: relative; flex: 0 0 auto; height: 100%; aspect-ratio: 3 / 4; margin-right: var(--space-p14); background: var(--surface-chrome); border-radius: var(--radius-option); overflow: hidden; box-shadow: var(--shadow-frame); }
.ss-live-clip { display: block; width: 100%; height: 100%; object-fit: contain; background: var(--surface-chrome); }
/* the panel: laid out once at the page's own pixel size, scaled to fit its pane; hidden until the walk arrives */
.ss-panel-wrap { position: relative; flex: 0 0 auto; height: 100%; aspect-ratio: var(--ss-panel-ar, 5 / 4); border-radius: var(--radius-option); overflow: hidden; background: var(--surface-chrome); box-shadow: var(--shadow-frame); opacity: 0; transition: opacity var(--dur-t260) var(--ease-standard); pointer-events: none; }
.is-arrived .ss-panel-wrap { opacity: 1; pointer-events: auto; }
/* the panel's box is its native size; its centre is put on the pane's centre and the scale is about that centre */
.ss-panel { position: absolute; left: 50%; top: 50%; transform-origin: center center; }
.ss-failed { position: absolute; left: var(--space-p16); bottom: var(--space-p16); margin: 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-on-navy); }
.pn-note.is-aside { position: static; max-width: none; transform: none; }
.pn-note.is-wrong { border-left-color: var(--incorrect); }
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--sim { container-type: normal; }
  /* in flow, the row stacks: the note, then the page at its own ratio, then the live view */
  .ss-frame { height: auto; }
  .ss-stage { flex-direction: column; height: auto; }
  /* the stack has no row to cover: the clip is its own block at its own ratio, first, and goes once arrived */
  .ss-clip { position: static; height: auto; aspect-ratio: var(--ss-clip-ar, 16 / 9); order: 0; margin-bottom: var(--space-p12); }
  .is-arrived .ss-clip { display: none; }
  .ss-note-col { flex: none; padding-right: 0; margin-bottom: var(--space-p12); }
  .ss-panel-wrap { width: 100%; height: auto; aspect-ratio: var(--ss-panel-ar, 5 / 4); order: 1; }
  .ss-live { width: 100%; height: auto; aspect-ratio: 3 / 4; max-height: 60vh; margin: var(--space-p12) 0 0; order: 2; }
}

/* ============================================================================
   THE CAMERA PANE (camera-pane.js, 2026-09-17): the record's photograph of the machine's camera under the
   drawn reticle; the photo slides by an offset, the pointer's tip is drawn where the camera would see it
   ============================================================================ */
.cam-pane { position: relative; flex: 0 0 auto; height: 100%; overflow: hidden; border-radius: var(--radius-option); background: var(--ink-deep); box-shadow: var(--shadow-frame); }
.cam-photo { position: absolute; left: 0; top: 0; width: 100%; height: 100%; object-fit: fill; user-select: none; }   /* left/top/width/height set inline: the zoom about the reticle plus the scene's shift */
.cam-lens { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; pointer-events: none; user-select: none; }   /* the exposure film, at the pane's frame (2026-09-18) */
.cam-reticle-h { position: absolute; left: 0; right: 0; height: var(--border-w2); background: var(--incorrect); pointer-events: none; }   /* w2: the HMI's lines are two bright pixels; a hairline vanished in the grain (2026-09-17) */
.cam-reticle-v { position: absolute; top: 0; bottom: 0; width: var(--border-w2); background: var(--incorrect); pointer-events: none; }
.cam-reticle-ring { position: absolute; border: var(--border-w2) solid var(--incorrect); border-radius: var(--radius-circle); pointer-events: none; }
.cam-pointer { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.cam-pointer-rod { fill: var(--grey-3); stroke: var(--ink-deep-a55); stroke-width: 1; }
.cam-pointer-tip { fill: var(--white); }
/* the target (2026-09-17, Tyler on the pendant screen: "paint ... aqua green, whatever that green color is that they use, with a thin
   crosshair in the middle of it as the target for where they need to move to"): a square in the signal colour, a hairline cross */
.cam-target { position: absolute; border: var(--border-w4) solid var(--signal); pointer-events: none; }   /* w4 (Tyler, 2026-09-17: "twice as thick to make it visible"); the cross inside stays a hairline */
.cam-target-h { position: absolute; left: 0; right: 0; top: 50%; height: var(--border-hairline); background: var(--signal); }
.cam-target-v { position: absolute; top: 0; bottom: 0; left: 50%; width: var(--border-hairline); background: var(--signal); }

/* ============================================================================
   POINTER + CAMERA (pointer-camera.js, 2026-09-17): the note | the part with the pointer | the camera - the
   two panes at full height, the row rule (softwareSim): the head's room or the width beside the column
   ============================================================================ */
.tpl-page--pointer, .tpl-page--pendant { display: flex; flex-direction: column; min-height: 100%; container-type: size; }
.pc-page, .pd-page { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-p14); }
.pc-head, .pd-head { text-align: center; flex-shrink: 0; }
.pc-eyebrow, .pd-eyebrow { font: var(--fw-extrabold) var(--type-vid-eyebrow)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-cover-eyebrow); text-transform: uppercase; color: var(--signal); }
.pc-title, .pd-title { margin: var(--space-p6) 0 0; font: var(--fw-extrabold) var(--type-vid-title)/var(--lh-heading) var(--font-ui); letter-spacing: var(--track-heading); color: var(--text-on-navy); }
.pc-frame, .pd-frame { position: relative; width: 100%; display: flex; flex-direction: row; align-items: stretch; gap: var(--space-p14);
  height: min(calc(100cqh - 130px), calc((100cqw - var(--size-p300) - 2 * var(--space-p14)) / var(--pc-panes, 2.6667))); }   /* the two panes' widths in heights (--pc-panes, set by the template from the pictures' sizes; two 4:3 panes = 8/3) beside the column and the two gaps */
.pc-note-col, .pd-note-col { flex: 1 1 var(--size-p300); min-width: var(--size-p300); }
.pc-part, .pd-pane { position: relative; flex: 0 0 auto; height: 100%; overflow: hidden; border-radius: var(--radius-option); background: var(--surface-chrome); box-shadow: var(--shadow-frame); }
.pc-part-img, .pd-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; user-select: none; }
.pc-rod { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.pc-rod-body { fill: var(--grey-3); stroke: var(--ink-deep-a55); stroke-width: 1; }
.pc-rod-handle { fill: var(--illus-royal); stroke: var(--ink-deep-a55); stroke-width: 1; }   /* the handle, the rod's lower half, wider (2026-09-19) */
/* the grip (2026-09-17, was .pc-tip - the hand held the tip; Tyler: "they can't see what they're pointing at"): the handle a way
   down the upright rod; the tip rides above it */
.pc-grip { position: absolute; transform: translate(-50%, -50%); width: var(--hit-target); height: var(--hit-target); cursor: grab; touch-action: none; }
.pc-grip:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; border-radius: var(--radius-circle); }
.pc-grip-ring { position: absolute; inset: var(--space-p10); border-radius: var(--radius-circle); border: var(--border-w2) solid var(--signal-a55); animation: ccPulse var(--dur-t600) var(--ease-in-out) infinite alternate; }
.pc-grip.is-done { cursor: default; } .pc-grip.is-done .pc-grip-ring { border-color: var(--correct); animation: none; }
.pc-part.is-carrying, .pc-part.is-carrying .pc-grip { cursor: grabbing; }   /* picked up by a click: it rides the pointer until the next click (2026-09-17) */
.pc-mark { position: absolute; transform: translate(-50%, -50%); width: var(--space-p14); height: var(--space-p14); border-radius: var(--radius-circle); border: var(--border-w2) solid var(--correct); pointer-events: none; }
/* in the flow shell the row stacks - the note, the part (or the pendant), the camera - each pane at its own ratio across the
   width (2026-09-17: without this the size container collapsed with the flow and both rows measured 0 px tall at 1024 / 768 / 390) */
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--pointer, .tpl-page--pendant { container-type: normal; }
  .pc-frame, .pd-frame { height: auto; flex-direction: column; }
  .pc-note-col, .pd-note-col { flex: none; min-width: 0; }
  .pc-part, .pd-pane, .tpl-page--pointer .cam-pane, .tpl-page--pendant .cam-pane { width: 100%; height: auto; }
}

/* ============================================================================
   THE PENDANT (pendant.js, 2026-09-17): the render with its controls laid over it at the render's points
   ============================================================================ */
.pd-readout { margin-top: var(--space-p12); display: flex; flex-direction: column; gap: var(--space-p4); font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--white-a92); }
.pd-readout-key { display: inline-flex; align-items: center; gap: var(--space-p6); color: var(--text-on-navy); }
.pd-readout-dot { width: var(--space-p10); height: var(--space-p10); border-radius: var(--radius-circle); background: var(--incorrect); box-shadow: 0 0 0 var(--border-hairline) var(--white-a55); }   /* the same dot as by the domes (2026-09-17: red text on the navy read badly) */
.pd-readout-key.is-held .pd-readout-dot { background: var(--correct); }
.pd-wheel-strip { position: absolute; inset: 0; pointer-events: none; }
.pd-still { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; opacity: 0; }
.pd-still.is-current { opacity: 1; }
.pd-knob { position: absolute; padding: 0; margin: 0; border: none; border-radius: var(--radius-circle); background: var(--ink-deep); box-shadow: inset 0 0 0 var(--border-w2) var(--black-a45); cursor: pointer; }
.pd-knob:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.pd-knob-pointer { position: absolute; left: 50%; top: 50%; width: var(--space-p4); height: 44%; margin-left: calc(-1 * var(--space-p4) / 2); background: var(--surface-card); transform-origin: 50% 0; border-radius: var(--radius-badge); }
.pd-wheel { position: absolute; border-radius: var(--radius-circle); cursor: grab; touch-action: none; }
.pd-wheel:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.pd-jog { position: absolute; transform: translate(-50%, -50%); width: var(--size-p70); height: var(--size-p70); padding: 0; margin: 0; border: none; background: transparent; border-radius: var(--radius-badge); cursor: pointer; }
.pd-jog:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
.pd-jog:active { box-shadow: inset 0 0 0 var(--border-w2) var(--signal-a55); }
/* the domes shaded (2026-09-17, was a dot beside each dome - Tyler: "shade the dead man switches red and have the red shading
   pulsate"): an ellipse the size of the dome (pd.domeSize, set inline), red and breathing until the key is held, green while it is */
.pd-deadman { position: absolute; border-radius: var(--radius-circle); background: var(--incorrect); opacity: var(--alpha-75); pointer-events: none; animation: pdBreathe var(--dur-t800) var(--ease-in-out) infinite alternate; }
.pd-deadman.is-held { background: var(--correct); opacity: var(--alpha-82); animation: none; }
@keyframes pdBreathe { from { opacity: var(--alpha-35); } to { opacity: var(--alpha-85); } }
/* the deadman status card on the pane, where Tyler drew his box ("a dead man switch status indicator that indicates that operation of
   the pendant doesn't work without pressing those dead man switches"): a lamp, the state, one line of why */
.pd-status { position: absolute; display: flex; flex-direction: column; justify-content: center; gap: var(--space-p8); padding: var(--space-p12); box-sizing: border-box; border-radius: var(--radius-option); background: var(--surface-card); box-shadow: var(--shadow-frame); color: var(--text-prose); pointer-events: none; }
.pd-status-key { display: flex; align-items: center; gap: var(--space-p8); font: var(--fw-extrabold) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-heading); }
.pd-status-lamp { flex: none; width: var(--space-p14); height: var(--space-p14); border-radius: var(--radius-circle); background: var(--incorrect); animation: pdBreathe var(--dur-t800) var(--ease-in-out) infinite alternate; }
.pd-status.is-held .pd-status-lamp { background: var(--correct); animation: none; }
.pd-status-body { margin: 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-prose); }
/* the foot of the pendant page (2026-09-17, Tyler's two arrows): the frame's height is a page variable so the sheet's column can be
   the note column's width; the Set button sits centred under the panes; the sheet's frame runs the page's full height at the
   column's width, so its tab sticks to the bottom chrome and its panel rises over the column */
.pd-page { --pd-h: min(calc(100cqh - 130px), calc((100cqw - var(--size-p300) - 2 * var(--space-p14)) / var(--pc-panes, 2.6667))); }
.pd-frame { height: var(--pd-h); }
.pd-actions { flex: 1 0 auto; display: flex; align-items: flex-start; justify-content: center; padding-top: var(--space-p16); }
.pd-set { flex: none; }   /* the learner's claim (2026-09-17: "a submit button instead of it automatically ending") - .cmp-btn's look */
.pd-set:disabled { opacity: var(--alpha-45); cursor: default; }
.pd-sheet-col { position: absolute; left: 0; top: 0; bottom: 0; width: calc(100% - var(--pd-h) * var(--pc-panes, 2.6667) - 2 * var(--space-p14)); pointer-events: none; }
@media (max-width: 1024px /* bp:tablet */) {
  .pd-page { --pd-h: auto; }
  .pd-actions { justify-content: center; }
  .pd-sheet-col { position: static; width: auto; pointer-events: auto; }
}

/* ============================================================================
   NOTEPAD REVEAL (notepad-reveal.js, 2026-09-17): the notepad in the left column - every number from the
   start, each line blank until its point is clicked - and the picture at full height beside it with a
   numbered marker on each point (Tyler: "a number on the left side, like a notepad ... blue for unrevealed
   to green for revealed"). The row rule is pointer + camera's: the head's room or the picture's width
   beside the column (--np-panes, the picture's width in heights, set by the template). Own prefix, no
   shared rule; the number and the marker read surface-chrome / text-on-navy unrevealed and
   signal / text-on-signal revealed, so a client's theme colours them by construction.
   ============================================================================ */
.tpl-page--notepad { display: flex; flex-direction: column; min-height: 100%; container-type: size; }
.np-page { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-p14); }
.np-head { text-align: center; flex-shrink: 0; }
.np-eyebrow { font: var(--fw-extrabold) var(--type-vid-eyebrow)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-cover-eyebrow); text-transform: uppercase; color: var(--signal); }
.np-title { margin: var(--space-p6) 0 0; font: var(--fw-extrabold) var(--type-vid-title)/var(--lh-heading) var(--font-ui); letter-spacing: var(--track-heading); color: var(--text-on-navy); }
.np-lead { margin: var(--space-p6) auto 0; max-width: var(--size-p860); font: var(--fw-medium) var(--type-lead)/var(--lh-text) var(--font-ui); color: var(--text-on-navy-soft); }   /* the instruction line, at most two lines (2026-09-18) */
.np-frame { position: relative; width: 100%; display: flex; flex-direction: row; align-items: stretch; gap: var(--space-p14);
  height: min(calc(100cqh - 130px), calc((100cqw - var(--size-p300) - var(--space-p14)) / var(--np-panes, 1.3333))); }   /* the picture's width in heights beside the column and the one gap */
/* the lead-in clip (2026-09-19): over the whole row while it plays, gone on arrival; the pad and the picture wait under it */
.np-clip { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: var(--radius-option); background: var(--surface-chrome); z-index: var(--z-l2); }
.tpl-page--notepad.is-arrived .np-clip { display: none; }
.tpl-page--notepad.has-lead .np-marker, .tpl-page--notepad.has-lead .np-leaders { visibility: hidden; }
/* THE NOTEPAD: the theme's card. The title and the foot stay put and the list between them scrolls when it is taller
   than the column - the done line in the foot has to be in view when the last point is noted */
.np-pad { flex: 1 1 var(--size-p300); min-width: var(--size-p300); height: 100%; display: flex; flex-direction: column; box-sizing: border-box; padding: var(--space-p16); background: var(--surface-card); border-radius: var(--radius-option); box-shadow: var(--shadow-frame); overflow: hidden; }
.np-pad-title { flex-shrink: 0; margin: 0 0 var(--space-p10); font: var(--fw-extrabold) var(--type-h3)/var(--lh-heading) var(--font-ui); color: var(--text-heading); }
.np-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--space-p8); }
.np-item { display: flex; align-items: flex-start; gap: var(--space-p10); }
/* the number: written from the start, the chrome's colour until its point is clicked, the signal once noted */
.np-num { flex: none; width: var(--size-p28); height: var(--size-p28); border-radius: var(--radius-circle); display: grid; place-items: center; background: var(--surface-chrome); color: var(--text-on-navy); font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui); }
.np-item.is-seen .np-num { background: var(--signal); color: var(--text-on-signal); }
.np-item.is-active .np-num { box-shadow: 0 0 0 var(--border-w2) var(--signal-a55); }
/* the line: a notepad's rule while blank, the name and what it does written on it once noted */
.np-text { flex: 1 1 auto; min-width: 0; min-height: var(--size-p28); box-sizing: border-box; padding: var(--space-p4) var(--space-p6) var(--space-p6); border-bottom: var(--border-hairline) solid var(--border); }
.np-item.is-active .np-text { background: var(--signal-a06); border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.np-label { font: var(--fw-extrabold) var(--type-body)/var(--lh-text) var(--font-ui); color: var(--text-heading); }
.np-body { margin: var(--space-p2) 0 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-prose); }
/* the foot: the hint and the count until every point is noted, then the done line */
.np-foot { flex-shrink: 0; display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-p10); margin-top: var(--space-p12); padding-top: var(--space-p12); border-top: var(--border-hairline) solid var(--border); }
.np-hint { font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-secondary); }
.np-count { flex: none; font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-title); color: var(--signal-text); }
.is-done .np-count { color: var(--correct); }
.np-foot-eyebrow { font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--signal-text); }
.np-foot-title { margin: var(--space-p2) 0 0; font: var(--fw-extrabold) var(--type-body)/var(--lh-heading) var(--font-ui); color: var(--text-heading); }
.np-foot-body { margin: var(--space-p4) 0 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-prose); }
/* ---- StepsPad (.sp-, 2026-09-18): the step-by-step instructions on the notepad, in softwareSim / focusLoop / watchedRun's
   note column (config.pad). The notepadReveal's pad look - the card, the numbered lines, the foot - under its own prefix.
   Fills the column: a plain column (softwareSim's) by height, a flex column (focusLoop's, watchedRun's) by growing. */
.sp-pad { height: 100%; display: flex; flex-direction: column; box-sizing: border-box; padding: var(--space-p16); background: var(--surface-card); border-radius: var(--radius-option); box-shadow: var(--shadow-frame); overflow: hidden; }
.fl-note-col .sp-pad, .wr-note-col .sp-pad, .cc-note-col .sp-pad, .pc-note-col .sp-pad, .pd-note-col .sp-pad { height: auto; flex: 1 1 auto; min-height: 0; }
.cc-frame.is-row .cc-note-col, .pc-note-col, .pd-note-col { display: flex; flex-direction: column; }   /* the pad grows, the pendant's readout under it (2026-09-19) */
.sp-lead { flex-shrink: 0; margin: 0 0 var(--space-p12); padding: 0 0 var(--space-p12); border-bottom: var(--border-hairline) solid var(--border); }   /* the why, above the heading (2026-09-19) */
.sp-lead-title { font: var(--fw-extrabold) var(--type-body)/var(--lh-text) var(--font-ui); color: var(--text-heading); }
.sp-lead-body { margin: var(--space-p2) 0 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-prose); }
.sp-pad-title { flex-shrink: 0; margin: 0 0 var(--space-p10); font: var(--fw-extrabold) var(--type-h3)/var(--lh-heading) var(--font-ui); color: var(--text-heading); }
.sp-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--space-p8); }
.sp-item { display: flex; align-items: flex-start; gap: var(--space-p10); }
.sp-num { flex: none; width: var(--size-p28); height: var(--size-p28); border-radius: var(--radius-circle); display: grid; place-items: center; background: var(--surface-chrome); color: var(--text-on-navy); font: var(--fw-extrabold) var(--type-small)/var(--lh-solid) var(--font-ui); }
.sp-item.is-done .sp-num { background: var(--signal); color: var(--text-on-signal); }
.sp-item.is-active .sp-num { box-shadow: 0 0 0 var(--border-w2) var(--signal-a55); }
.sp-text { flex: 1 1 auto; min-width: 0; min-height: var(--size-p28); box-sizing: border-box; padding: var(--space-p4) var(--space-p6) var(--space-p6); border-bottom: var(--border-hairline) solid var(--border); }
.sp-item.is-active .sp-text { background: var(--signal-a06); border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.sp-label { font: var(--fw-medium) var(--type-body)/var(--lh-text) var(--font-ui); color: var(--text-heading); }   /* the key alone is bold (2026-09-19) */
.sp-key { font-weight: var(--fw-extrabold); }
.sp-body { margin: var(--space-p2) 0 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-prose); }
.sp-bullets, .pn-bullets { padding-left: var(--space-p18); list-style: disc; }   /* a body as bullet points (2026-09-19) */
.sp-bullets li, .pn-bullets li { margin: var(--space-p2) 0 0; }
.sp-keys { margin: var(--space-p4) 0 0; font: var(--fw-medium) var(--type-caption)/var(--lh-text) var(--font-ui); color: var(--text-secondary); }
.sp-foot { flex-shrink: 0; margin-top: var(--space-p12); padding-top: var(--space-p12); border-top: var(--border-hairline) solid var(--border); }
.sp-count { font: var(--fw-extrabold) var(--type-caption)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-title); color: var(--signal-text); }
.sp-foot-eyebrow { font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--signal-text); }
.sp-foot-note.is-wrong .sp-foot-eyebrow { color: var(--incorrect); }
.sp-foot-note.is-done .sp-foot-eyebrow { color: var(--correct); }
.sp-foot-title { margin: var(--space-p2) 0 0; font: var(--fw-extrabold) var(--type-body)/var(--lh-heading) var(--font-ui); color: var(--text-heading); }
.sp-foot-body { margin: var(--space-p4) 0 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-prose); }
/* THE PICTURE at its own ratio (aspect-ratio set inline from scene.size), the markers on its points (left / top inline,
   percentages of scene.size; the size is `hit` scene px, never below the touch target) */
.np-scene { position: relative; flex: 0 0 auto; height: 100%; overflow: hidden; border-radius: var(--radius-option); background: var(--surface-chrome); box-shadow: var(--shadow-frame); }
.np-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; user-select: none; }
/* a leader from a stood-off marker (items[].at) to its point: a hairline and a dot, navy until noted, the signal colour after */
.np-leaders { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; z-index: var(--z-l1); }
.np-leader line { stroke: var(--surface-chrome); stroke-width: 2; }
.np-leader circle { fill: var(--surface-chrome); stroke: var(--white); stroke-width: 1; }
.np-leader.is-seen line { stroke: var(--signal); } .np-leader.is-seen circle { fill: var(--signal); }
/* a marker: a numbered circle in the pad's colours, a pulsing ring (the grip's, ccPulse) until its line is noted */
.np-marker { position: absolute; transform: translate(-50%, -50%); padding: 0; margin: 0; border: var(--border-w2) solid var(--white); border-radius: var(--radius-circle); background: var(--surface-chrome); color: var(--text-on-navy); box-shadow: 0 2px 8px var(--black-a45); cursor: pointer; z-index: var(--z-l2); display: grid; place-items: center; font: var(--fw-extrabold) var(--type-body)/var(--lh-solid) var(--font-ui); }
.np-marker-ring { position: absolute; inset: calc(-1 * var(--space-p6)); border-radius: var(--radius-circle); border: var(--border-w2) solid var(--signal-a55); animation: ccPulse var(--dur-t600) var(--ease-in-out) infinite alternate; pointer-events: none; }
.np-marker-num { position: relative; }
.np-marker.is-seen { background: var(--signal); color: var(--text-on-signal); }
.np-marker.is-seen .np-marker-ring { display: none; }
.np-marker.is-active { box-shadow: 0 0 0 var(--border-w3) var(--white-a55), 0 2px 8px var(--black-a45); }
.np-marker:hover .np-marker-ring, .np-marker:focus-visible .np-marker-ring { animation: none; border-color: var(--signal); }
.np-marker:focus-visible { outline: var(--border-focus) solid var(--white); outline-offset: 2px; }
/* in the flow shell the row stacks: the picture first at full width and its ratio, the pad below it at full width (the
   pointer + camera block's rule - a size container collapses in the flow) */
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--notepad { container-type: normal; }
  .np-frame { height: auto; flex-direction: column; }
  .np-scene { order: 0; width: 100%; height: auto; }
  .np-pad { order: 1; flex: none; min-width: 0; height: auto; overflow: visible; }
  .np-list { overflow-y: visible; }
}
@media (prefers-reduced-motion: reduce) {
  .np-marker-ring { animation: none; }
}

/* ============================================================================
   THE REFERENCE SHEET (ref-sheet.js, 2026-09-17): the pull-up reference at the foot of a note column - Tyler:
   "a bottom tab, right above the back button, centred on that left column, that says 'Pendant controls'. If
   they click that tab, it opens and shows them what the controls do again if they forgot. Also make it click
   and draggable, like you can left click and drag it up. And then when you click the tab for it again, it
   retracts." A component a template composes: the column carries .rs-host (the tab's room) and is the sheet's
   frame; the tab is a pill on the column's bottom edge, the panel the theme's card that slides up over the
   column's content and scrolls inside. THE TRANSITION carries visibility with the transform: a closed panel
   turns hidden at the END of its slide down (out of the tab order) and visible at the START of its slide up.
   Everything below this banner is the sheet's; nothing above it is.
   ============================================================================ */
.pd-note-col, .pc-note-col { position: relative; }   /* the sheet's frame: it is pinned to the column's bottom edge */
.rs-host { padding-bottom: var(--size-p56); }        /* the tab's room - hit-target tall, on the edge - so the column's note never sits under it */
/* column-reverse: the tab (first in the DOM, so first in the tab order) at the bottom, the panel above it; the frame itself
   takes no pointer, so the note under a closed sheet stays selectable; overflow hidden clips the panel's slide below the column */
.rs { position: absolute; left: 0; right: 0; bottom: 0; max-height: 100%; display: flex; flex-direction: column-reverse; align-items: stretch; overflow: hidden; pointer-events: none; }
.rs-tab {
  pointer-events: auto; position: relative; z-index: var(--z-l2); flex: none; align-self: center;
  display: inline-flex; align-items: center; gap: var(--space-p8); min-height: var(--hit-target); margin: 0; padding: var(--space-p8) var(--space-p18);
  border: var(--border-hairline) solid var(--white-a16); border-bottom: 0; border-radius: var(--radius-badge) var(--radius-badge) 0 0;   /* the edge: the pill sits on the stage's own navy */
  background: var(--surface-chrome); color: var(--text-on-navy); box-shadow: var(--shadow-frame); cursor: pointer; touch-action: none; user-select: none;
  font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-label); text-transform: uppercase;
}
.rs-tab:focus-visible { outline: var(--border-focus) solid var(--signal); outline-offset: 2px; }
/* the chevron: a bordered corner turned to point UP while closed (pull me up) and DOWN while open */
.rs-tab::before { content: ""; flex: none; width: var(--space-p8); height: var(--space-p8); border-left: var(--border-w2) solid currentColor; border-top: var(--border-w2) solid currentColor; transform: translateY(25%) rotate(45deg); transition: transform var(--dur-t260) var(--ease-standard); }
.rs.is-open .rs-tab::before { transform: translateY(-25%) rotate(225deg); }
.rs-panel {
  pointer-events: auto; position: relative; z-index: var(--z-l1); flex: 0 1 auto; min-height: 0; overflow-y: auto; box-sizing: border-box;
  margin: 0; padding: var(--space-p16) var(--space-p18) var(--space-p14);
  background: var(--surface-card); color: var(--text-body); border-radius: var(--radius-option); box-shadow: var(--shadow-frame);
  transform: translateY(100%); visibility: hidden;
  transition: transform var(--dur-t260) var(--ease-standard), visibility var(--dur-t260);
}
.rs.is-open .rs-panel { transform: translateY(0); visibility: visible; }
.rs.is-dragging .rs-panel { transition: none; visibility: visible; }   /* the finger has it: the transform is inline and no easing sits between its positions */
.rs-title { margin: 0 0 var(--space-p10); font: var(--fw-extrabold) var(--type-h5)/var(--lh-heading) var(--font-ui); letter-spacing: var(--track-heading-sm); color: var(--text-heading); }
.rs-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-p10); }
.rs-item { display: flex; align-items: flex-start; gap: var(--space-p10); }
.rs-num { flex: none; display: grid; place-items: center; width: var(--size-p26); height: var(--size-p26); border-radius: var(--radius-circle); background: var(--signal); color: var(--text-on-signal); font: var(--fw-extrabold) var(--type-micro)/var(--lh-solid) var(--font-ui); }
.rs-text { min-width: 0; }
.rs-label { font: var(--fw-extrabold) var(--type-copy)/var(--lh-snug) var(--font-ui); color: var(--text-heading); }
.rs-body { margin-top: var(--space-p2); font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-prose); }
/* the flow shell: the column is in flow, so the sheet is a block after its content - the tab, then the panel shown or not;
   no slide, no drag (ref-sheet.js starts none under BP.tablet) */
@media (max-width: 1024px /* bp:tablet */) {
  .rs-host { padding-bottom: 0; }
  .rs { position: static; max-height: none; flex-direction: column; overflow: visible; pointer-events: auto; margin-top: var(--space-p12); }
  .rs-tab { border-bottom: var(--border-hairline) solid var(--white-a16); border-radius: var(--radius-badge); touch-action: auto; }
  .rs-panel { display: none; transform: none; visibility: visible; transition: none; margin-top: var(--space-p12); }
  .rs.is-open .rs-panel { display: block; }
}
@media (prefers-reduced-motion: reduce) {
  .rs-panel, .rs-tab::before { transition: none; }
}

/* ============================================================================
   FOCUS LOOP (focus-loop.js, 2026-09-17): the note column with Set under it | the camera, its photo blurred
   by the head's distance from focus (the blur is an inline filter the template computes; the reticle over it
   stays sharp - "you move the head, not the camera") | the machine's page (course media in a shadow root,
   scaled to fit, as softwareSim). The row rule is pointer + camera's: the head's room or the two panes' width
   beside the column (--fl-panes from the camera's and the page's sizes). The coach's note is the pop-up
   note with a second edge colour - the correct green, not the wrong red: it advises, it does not refuse.
   ============================================================================ */
.tpl-page--focus { display: flex; flex-direction: column; min-height: 100%; container-type: size; }
.fl-page { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-p14); }
.fl-head { text-align: center; flex-shrink: 0; }
.fl-eyebrow { font: var(--fw-extrabold) var(--type-vid-eyebrow)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-cover-eyebrow); text-transform: uppercase; color: var(--signal); }
.fl-title { margin: var(--space-p6) 0 0; font: var(--fw-extrabold) var(--type-vid-title)/var(--lh-heading) var(--font-ui); letter-spacing: var(--track-heading); color: var(--text-on-navy); }
.fl-frame { position: relative; width: 100%; display: flex; flex-direction: row; align-items: stretch; gap: var(--space-p14);
  height: min(calc(100cqh - 130px), calc((100cqw - var(--size-p300) - 2 * var(--space-p14)) / var(--fl-panes, 2.5833))); }   /* the two panes' widths in heights (a 4:3 camera and a 5:4 page = 31/12) beside the column and the two gaps */
.fl-note-col { flex: 1 1 var(--size-p300); min-width: var(--size-p300); display: flex; flex-direction: column; }
/* SET under the note (the pendant's button, the learner's claim: "a submit button instead of it automatically ending") */
.fl-actions { flex: none; display: flex; justify-content: center; padding-top: var(--space-p16); }
.fl-set { flex: none; }
.fl-set:disabled { opacity: var(--alpha-45); cursor: default; }
/* the page: laid out once at its own pixel size, scaled to fit its pane (the box is its native size; its centre on the
   pane's centre and the scale about that centre - softwareSim's .ss-panel) */
.fl-panel-wrap { position: relative; flex: 0 0 auto; height: 100%; aspect-ratio: var(--fl-panel-ar, 5 / 4); border-radius: var(--radius-option); overflow: hidden; background: var(--surface-chrome); box-shadow: var(--shadow-frame); }
.fl-panel { position: absolute; left: 50%; top: 50%; transform-origin: center center; }
.fl-failed { position: absolute; left: var(--space-p16); bottom: var(--space-p16); margin: 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-on-navy); }
/* the coach's note: the card's edge and eyebrow in the correct colour - advice after a stall, never a refusal (that edge is
   .is-wrong's, the incorrect colour, and it goes on its own) */
.pn-note.is-coach { border-left-color: var(--correct); }
.pn-note.is-coach .pn-eyebrow { color: var(--correct); }
/* in the flow shell the row stacks - the note and Set, the camera, the page - each pane at its own ratio across the width
   (the pointer + camera block's rule: a size container collapses in the flow) */
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--focus { container-type: normal; }
  .fl-frame { height: auto; flex-direction: column; }
  .fl-note-col { flex: none; min-width: 0; }
  .tpl-page--focus .cam-pane, .fl-panel-wrap { width: 100%; height: auto; }
}

/* ============================================================================
   WATCHED RUN (watched-run.js, 2026-09-18): the note column - the instruction, then THE CALL under it once the
   timeline has ended (per pass inside / out, the third of the turn when out, Make the call), then the verdict's
   note with Run it again or Watch it again | the live view (the run, a <video> the timeline's play entry starts)
   | the machine's page (course media in a shadow root, scaled to fit, as softwareSim). The row rule is softwareSim's
   live layout, copied: the head's room, or the two panes' width (3/4 + 5/4 = 2 heights) beside the note column,
   whichever is smaller, so nothing moves when the note's text or the call's buttons change. The wobble itself is
   the strip's paint (software-sim.js applyStrip) inside the course's page - nothing of it is styled here. Tyler,
   2026-09-18: "design a fail state track mode for this one. a wobble".
   ============================================================================ */
.tpl-page--watched { display: flex; flex-direction: column; min-height: 100%; container-type: size; }
.wr-page { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-p14); }
.wr-head { text-align: center; flex-shrink: 0; }
.wr-eyebrow { font: var(--fw-extrabold) var(--type-vid-eyebrow)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-cover-eyebrow); text-transform: uppercase; color: var(--signal); }
.wr-title { margin: var(--space-p6) 0 0; font: var(--fw-extrabold) var(--type-vid-title)/var(--lh-heading) var(--font-ui); letter-spacing: var(--track-heading); color: var(--text-on-navy); }
.wr-frame {
  position: relative; flex: 0 0 auto; width: 100%;
  height: min(calc(100cqh - 92px), calc((100cqw - var(--size-p300)) / 2));   /* 92: the head (one or two lines) and the gap - softwareSim's number */
}
.wr-stage { position: relative; width: 100%; height: 100%; display: flex; flex-direction: row; align-items: stretch; }
/* the column scrolls when the call's rows outgrow the row's height (three passes, or a narrow canvas); the panes never move for it */
.wr-note-col { flex: 1 1 var(--size-p300); min-width: var(--size-p300); padding-right: var(--space-p14); display: flex; flex-direction: column; overflow-y: auto; }
.wr-live { position: relative; flex: 0 0 auto; height: 100%; aspect-ratio: 3 / 4; margin-right: var(--space-p14); background: var(--surface-chrome); border-radius: var(--radius-option); overflow: hidden; box-shadow: var(--shadow-frame); }
.wr-live-clip { display: block; width: 100%; height: 100%; object-fit: contain; background: var(--surface-chrome); }
/* the page: laid out once at its own pixel size, scaled to fit its pane (the box is its native size; its centre on the
   pane's centre and the scale about that centre - softwareSim's .ss-panel); no lead-in here, so never hidden */
.wr-panel-wrap { position: relative; flex: 0 0 auto; height: 100%; aspect-ratio: var(--wr-panel-ar, 5 / 4); border-radius: var(--radius-option); overflow: hidden; background: var(--surface-chrome); box-shadow: var(--shadow-frame); }
.wr-panel { position: absolute; left: 50%; top: 50%; transform-origin: center center; }
.wr-failed { position: absolute; left: var(--space-p16); bottom: var(--space-p16); margin: 0; font: var(--fw-medium) var(--type-small)/var(--lh-text) var(--font-ui); color: var(--text-on-navy); }
/* THE CALL: one fieldset per pass under the note - its label on the stage, its options as pills; a picked option takes the
   signal fill (the theme's button), so the chosen reading is the one thing lit in the column */
.wr-call-form { flex: none; display: flex; flex-direction: column; gap: var(--space-p12); margin-top: var(--space-p14); }
.wr-pass { margin: 0; padding: 0; border: 0; min-width: 0; }
.wr-pass-label { padding: 0; margin: 0 0 var(--space-p6); font: var(--fw-extrabold) var(--type-label)/var(--lh-solid) var(--font-ui); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text-on-navy); }
.wr-row { display: flex; flex-wrap: wrap; gap: var(--space-p8); }
.wr-thirds { margin-top: var(--space-p8); }
.wr-opt {
  flex: 1 1 auto; border: var(--border-w2) solid var(--signal-a55); border-radius: var(--radius-pill); padding: var(--space-p8) var(--space-p14); cursor: pointer;
  font: var(--fw-bold) var(--type-small)/var(--lh-solid) var(--font-ui); background: var(--surface-card); color: var(--text-heading);
  transition: background var(--dur-t160) var(--ease-standard), border-color var(--dur-t160) var(--ease-standard);
}
.wr-opt:hover { border-color: var(--signal); }
.wr-opt:focus-visible { outline: none; box-shadow: 0 0 0 var(--border-w3) var(--signal-a55); }
.wr-opt.is-picked { background: var(--signal); border-color: var(--signal); color: var(--text-on-signal); }
/* Make the call, Run it again, Watch it again: the theme's button (.cmp-btn), centred under the column's content */
.wr-actions { flex: none; display: flex; justify-content: center; padding-top: var(--space-p12); }
.wr-call, .wr-again, .wr-watch-again { flex: none; }
.wr-call:disabled { opacity: var(--alpha-45); cursor: default; }
/* the verdict's note: a caught failure takes the correct green on its edge and eyebrow (the right call, not a refusal);
   a missed one is .is-wrong's red edge (shared, softwareSim's) */
.pn-note.is-caught { border-left-color: var(--correct); }
.pn-note.is-caught .pn-eyebrow { color: var(--correct); }
/* in the flow shell the row stacks - the note and the call, the page at its own ratio, then the live view - softwareSim's
   stack (a size container collapses in the flow) */
@media (max-width: 1024px /* bp:tablet */) {
  .tpl-page--watched { container-type: normal; }
  .wr-frame { height: auto; }
  .wr-stage { flex-direction: column; height: auto; }
  .wr-note-col { flex: none; padding-right: 0; margin-bottom: var(--space-p12); overflow-y: visible; }
  .wr-panel-wrap { width: 100%; height: auto; aspect-ratio: var(--wr-panel-ar, 5 / 4); order: 1; }
  .wr-live { width: 100%; height: auto; aspect-ratio: 3 / 4; max-height: 60vh; margin: var(--space-p12) 0 0; order: 2; }
}
