/* 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, #1A2E60);
  /* 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: 6px 28px 10px;
}

/* 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, #1A2E60) !important; padding-top: 8px !important; }
.tpl-page--scorecard > div { padding-top: 8px !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: 14px clamp(12px, 2vw, 28px) 18px; }
.tpl-page--guided-stepper > div {
  flex: 1; min-height: 0; width: 100%; max-width: 1180px; margin-inline: auto;
  border-radius: 16px; overflow: hidden; box-shadow: 0 8px 26px rgba(8, 12, 26, 0.3);
}

/* ---- Guided Steps: responsive (tablet + phone) — the whole page fits with NO scrolling ---- */
@media (max-width: 1024px) {
  /* 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, 56px) - 96px);
    min-height: 0; overflow: hidden;
    padding: 10px clamp(10px, 2vw, 20px) 12px;
  }
  /* 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: 52px !important; height: 52px !important; min-height: 52px !important;
    padding: 0 !important; border-radius: 50% !important; flex-shrink: 0;
    justify-content: center !important; gap: 0 !important; font-size: 0 !important;
  }
  .tpl-page--guided-stepper .gs-nav svg { width: 24px !important; height: 24px !important; }

  /* tighten the step control bar (the div with the border-top seam) */
  .tpl-page--guided-stepper div[style*="border-top"] { padding: 10px 16px 12px !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) {
  .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) {
  /* 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: 18px 14px 24px !important; }
  .tpl-page--sequence h2 { font-size: 22px !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: 80;
  display: flex; align-items: center; justify-content: center; padding: 20px;
  background: rgba(8, 12, 26, 0.45);
  font-family: 'Archivo', system-ui, sans-serif;
}
.gs-reveal-card {
  position: relative; width: 100%; max-width: 460px; max-height: 76vh; overflow: auto;
  background: var(--surface-stage, #1A2E60);
  border: 1px solid rgba(18, 166, 106, 0.45); border-top: 4px solid #12A66A;
  border-radius: 14px; padding: 24px; box-shadow: 0 18px 50px rgba(8, 12, 26, 0.55);
}
.gs-reveal-close {
  position: absolute; top: 6px; right: 8px; width: 38px; height: 38px;
  border: none; background: transparent; color: rgba(255, 255, 255, 0.65); font-size: 28px; line-height: 1;
  cursor: pointer; border-radius: 8px;
}
.gs-reveal-close:hover { color: #fff; }
.gs-reveal-label {
  font-weight: 800; font-size: 14px; letter-spacing: 0.4px; color: #35D18A;
  text-transform: uppercase; margin: 0 32px 10px 0;
}
.gs-reveal-body { font-size: 17px; line-height: 1.55; color: rgba(255, 255, 255, 0.92); }

/* 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);
}
@media (max-width: 1024px) {
  .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) {
  .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). */
.tpl-page h2 { font-size: 26px !important; line-height: 1.2 !important; }
.tpl-page h2 + p { margin: 2px 0 6px !important; font-size: 15px !important; line-height: 1.4 !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: 1426px !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) {
  .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) {
  /* 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) {
  .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) {
  /* 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: 40;
    top: var(--cm-header-h, 56px); bottom: 0; right: 0;
    width: min(88vw, 360px);
    transform: translateX(100%);
    transition: transform 240ms cubic-bezier(.2,.7,.3,1);
    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: -40px; top: 16px; width: 40px;
    margin: 0; border: 0; padding: 14px 8px;
    /* 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, #00A3DD); color: var(--navy, #132554);
    font: 700 13px/1 system-ui, sans-serif; letter-spacing: .02em;
    writing-mode: vertical-rl; text-orientation: mixed;
    border-radius: 10px 0 0 10px; box-shadow: -3px 0 10px rgba(0,0,0,.28);
    cursor: pointer; pointer-events: auto;
  }
  .cm-cat-tab:focus-visible { outline: 3px solid #fff; 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(--white, #fff); box-shadow: -8px 0 24px rgba(10,16,35,.35);
    padding: 18px 18px 24px;
  }
  .cm-cat-drawer-head { margin: 0 0 12px; font: 800 18px/1.2 system-ui, sans-serif; color: var(--navy, #132554); }
  .cm-cat-drawer-head:focus-visible { outline: 3px solid var(--blue, #00A3DD); outline-offset: 3px; border-radius: 4px; }
  .cm-cat-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
  /* color is a per-category ACCENT only; meaning is always the label + description text */
  .cm-cat-item { border-left: 4px solid var(--cm-cat-color, var(--navy, #132554)); background: rgba(19,37,84,.04); border-radius: 8px; padding: 10px 12px; }
  .cm-cat-item-name { display: block; font: 800 15px/1.2 system-ui, sans-serif; color: var(--cm-cat-color, var(--navy, #132554)); }
  .cm-cat-item-def  { display: block; margin-top: 4px; font: 500 14px/1.45 system-ui, sans-serif; color: var(--ink, #16203A); }
}

@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: #fff; border: 1px solid var(--border, #DCE2EC); border-radius: 20px;
  padding: clamp(36px, 6vw, 56px) clamp(28px, 6vw, 64px);
  text-align: center; max-width: 720px; width: 100%;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
}
.cmp-check {
  width: 84px; height: 84px; border-radius: 50%; margin: 0 auto 24px;
  background: rgba(18, 166, 106, 0.10); border: 2px solid var(--correct, #12A66A); color: var(--correct, #12A66A);
  display: flex; align-items: center; justify-content: center; font: 800 44px/1 var(--font-ui, 'Archivo', sans-serif);
}
.cmp-eyebrow {
  font: 800 15px/1 var(--font-ui, 'Archivo', sans-serif); letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--signal, #00A3DD); margin-bottom: 8px;
}
.cmp-title { font: 800 30px/1.15 var(--font-ui, 'Archivo', sans-serif); color: var(--navy, #132554); letter-spacing: -0.5px; margin-bottom: 6px; }
.cmp-body { font: 400 18px/1.6 var(--font-ui, 'Archivo', sans-serif); color: var(--secondary, #5A6577); margin: 12px 0 28px; }
.cmp-btn {
  border: none; border-radius: var(--radius-pill, 100px); padding: 15px 40px; cursor: pointer;
  font: 800 16px/1 var(--font-ui, 'Archivo', sans-serif); letter-spacing: 0.3px;
  background: var(--signal, #00A3DD); color: var(--navy, #132554);
}
.cmp-btn:hover { background: var(--signal-hover, #008EC1); }
.cmp-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(0, 163, 221, 0.55); }

/* ===== 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, 32px);
  align-items: flex-start;
  padding: 24px 32px 32px;
  min-height: 0;
}

/* Left summary card */
.sc-summary {
  width: 420px;
  flex-shrink: 0;
  background: var(--white, #fff);
  border: 1px solid var(--border, #DCE2EC);
  border-radius: var(--radius-card, 16px);
  padding: 40px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  justify-content: center;
  gap: 6px;
  box-shadow: var(--shadow-card, 0 1px 2px rgba(19,37,84,0.04));
}
.sc-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font: var(--fw-extrabold, 800) 14px/1 var(--font-ui, system-ui, sans-serif);
  letter-spacing: 1px;
  text-transform: uppercase;
  border-radius: var(--radius-pill, 100px);
  border: 1.5px solid;
  padding: 8px 20px;
  margin-bottom: 12px;
}
.sc-badge.is-pass { color: var(--correct, #12A66A); border-color: var(--correct, #12A66A); background: var(--correct-wash, rgba(18,166,106,0.10)); }
.sc-badge.is-fail { color: var(--incorrect, #E5463E); border-color: var(--incorrect, #E5463E); background: var(--incorrect-wash, rgba(229,70,62,0.07)); }
.sc-badge-dot {
  width: 22px; height: 22px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; color: var(--white, #fff);
}
.sc-badge.is-pass .sc-badge-dot { background: var(--correct, #12A66A); }
.sc-badge.is-fail .sc-badge-dot { background: var(--incorrect, #E5463E); }
.sc-score { font: var(--fw-black, 900) 96px/1 var(--font-ui, system-ui, sans-serif); color: var(--navy, #132554); letter-spacing: -2px; }
.sc-score .sc-denom { color: var(--disabled-btn, #B7C0CE); font-weight: var(--fw-extrabold, 800); }
.sc-score-label { font: 500 18px/1.3 var(--font-ui, system-ui, sans-serif); color: var(--secondary, #5A6577); }
.sc-stats {
  display: flex; gap: 40px; justify-content: center; align-items: stretch;
  width: 100%; margin-top: 24px; padding-top: 24px;
  border-top: 1px solid var(--page, #EEF1F6);
}
.sc-stat-val { font: var(--fw-extrabold, 800) 28px/1 var(--font-ui, system-ui, sans-serif); color: var(--signal, #00A3DD); }
.sc-stat-val.is-navy { color: var(--navy, #132554); }
.sc-stat-label { font: 600 13px/1.2 var(--font-ui, system-ui, sans-serif); color: var(--muted, #8A93A3); margin-top: 2px; }
.sc-stat-sep { width: 1px; background: var(--page, #EEF1F6); }

/* Right review list */
.sc-review { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.sc-review-title {
  font: var(--fw-extrabold, 800) 14px/1 var(--font-ui, system-ui, sans-serif);
  letter-spacing: var(--track-eyebrow, 1.5px); text-transform: uppercase;
  color: var(--signal, #00A3DD); margin-bottom: 16px;
}
.sc-rows { display: flex; flex-direction: column; gap: 14px; }
.sc-row {
  display: flex; align-items: center; gap: 18px;
  background: var(--white, #fff);
  border: 1.5px solid var(--border, #DCE2EC);
  border-radius: var(--radius-option, 12px);
  padding: 18px 22px;
}
.sc-row.is-wrong { background: var(--incorrect-wash, rgba(229,70,62,0.05)); border-color: var(--incorrect, #E5463E); }
.sc-mark {
  width: 34px; height: 34px; flex-shrink: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font: var(--fw-extrabold, 800) 16px/1 var(--font-ui, system-ui, sans-serif);
  color: var(--white, #fff); background: var(--correct, #12A66A);
}
.sc-row.is-wrong .sc-mark { background: var(--incorrect, #E5463E); }
.sc-q { flex: 1; min-width: 0; }
.sc-q-text { font: var(--fw-bold, 700) 18px/1.3 var(--font-ui, system-ui, sans-serif); color: var(--ink, #16203A); margin-bottom: 3px; }
.sc-q-chose { font: 400 15px/1.4 var(--font-ui, system-ui, sans-serif); color: var(--secondary, #5A6577); }
.sc-q-correct { font: 600 14px/1.4 var(--font-ui, system-ui, sans-serif); color: var(--correct, #12A66A); margin-top: 3px; }
/* 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: 400 14px/1.5 var(--font-ui, system-ui, sans-serif); color: var(--secondary, #5A6577);
  margin-top: 7px; padding-left: 11px; border-left: 3px solid var(--line, #DCE2EC);
}
.sc-row.is-wrong .sc-q-feedback { border-left-color: rgba(229, 70, 62, 0.45); }
.sc-verdict {
  flex-shrink: 0;
  font: var(--fw-extrabold, 800) 13px/1 var(--font-ui, system-ui, sans-serif);
  letter-spacing: 0.5px; color: var(--correct, #12A66A);
}
.sc-row.is-wrong .sc-verdict { color: var(--incorrect, #E5463E); }
.sc-empty {
  margin: auto; max-width: 520px; text-align: center;
  color: var(--text-on-navy, #fff);
  font: 500 17px/1.6 var(--font-ui, system-ui, sans-serif);
}

/* 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: 22px; padding-top: 20px;
  border-top: 1px solid var(--border, #DCE2EC);
  width: 100%; text-align: center;
}
.sc-fail-title {
  font: var(--fw-extrabold, 800) 20px/1.25 var(--font-ui, system-ui, sans-serif);
  color: var(--incorrect, #E5463E); margin-bottom: 8px;
}
.sc-fail-body {
  font: 400 15px/1.55 var(--font-ui, system-ui, sans-serif);
  color: var(--secondary, #5A6577); margin: 0 0 18px;
}
.sc-fail-btn {
  font: var(--fw-bold, 700) 15px/1 var(--font-ui, system-ui, sans-serif);
  color: #fff; background: var(--signal, #00A3DD);
  border: none; border-radius: var(--radius-btn, 999px);
  padding: 14px 28px; cursor: pointer;
}
.sc-fail-btn:hover { background: var(--signal-hover, #008EC1); }
.sc-fail-btn:active { background: var(--signal-pressed, #007BA6); }

/* 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) {
  .sc { flex-direction: column; align-items: stretch; padding: 20px 24px 28px; }
  .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) {
  .sc { padding: 16px 16px 24px; }
  .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: 8px; }   /* .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: 370ms;
  display: flex;
  gap: 0;
  min-height: 0;
  height: 100%;
  background: var(--surface-deep, #0E1C42);
  border-radius: var(--radius-card, 16px);
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.10);
}

/* Left illustration panel — swaps with the open row */
.acc-media {
  width: 46%;
  flex-shrink: 0;
  background: var(--surface-deep, #0E1C42);
  border-right: 1px solid rgba(255, 255, 255, 0.10);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
}
.acc-media-frame {
  width: 100%;
  aspect-ratio: 8 / 3;   /* matches the authored scene art (1600×600); wide, subject left-anchored */
  position: relative;
  overflow: hidden;
  border-radius: 12px;
  background: #E7EBF1;
  box-shadow: inset 0 2px 22px rgba(19, 37, 84, 0.14);
  border: 1px solid #D5DCE8;
  /* 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, 370ms) ease-in-out;   /* keyframes in player.css */
}
.acc-media-frame.is-leaving { animation: accMediaOut var(--acc-phase, 370ms) ease-in-out both; }
@keyframes accMediaOut { from { opacity: 1; } to { opacity: 0; } }
.acc-media-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* Right accordion column */
.acc-list-col {
  flex: 1;
  min-width: 0;
  padding: 40px 56px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.acc-eyebrow {
  font: var(--fw-extrabold, 800) 16px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  letter-spacing: 1.5px; text-transform: uppercase; color: var(--signal, #00A3DD);
}
.acc-heading {
  font-family: var(--font-display, 'Archivo', sans-serif);
  font-weight: 800; font-size: 34px; line-height: 1.15;
  color: #fff; margin: 6px 0 20px;
}
/* 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, 370ms) ease-in-out var(--acc-phase, 370ms) both;
}
/* optional instruction line under the heading (config.subtitle, 2026-07-26) */
.acc-subtitle {
  margin: -10px 0 18px;
  font: 400 17px/1.5 var(--font-ui, 'Archivo', sans-serif);
  color: rgba(255, 255, 255, 0.82);
}
.acc-list { display: flex; flex-direction: column; }
.acc-item { border-bottom: 1px solid rgba(255, 255, 255, 0.12); }
/* 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, 370ms) 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: 20px 4px; display: flex; align-items: center; gap: 16px;
}
.acc-num {
  width: 30px; height: 30px; flex-shrink: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font: 800 15px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  background: rgba(255, 255, 255, 0.10); color: rgba(255, 255, 255, 0.7);
}
/* 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, #00A3DD); color: var(--navy, #132554); }
.acc-title { flex: 1; font: 700 21px/1.25 var(--font-ui, 'Archivo', system-ui, sans-serif); color: #fff; }
.acc-toggle {
  font-size: 30px; font-weight: 400; color: var(--signal, #00A3DD);
  line-height: 1; width: 24px; text-align: center; flex-shrink: 0;
}
.acc-body {
  padding: 0 4px 0 46px;
  font: 400 18px/1.6 var(--font-ui, 'Archivo', system-ui, sans-serif);
  color: rgba(255, 255, 255, 0.8);
  /* 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, 370ms) ease-in-out;
}
.acc-body-wrap.is-open .acc-body { padding-bottom: 22px; }
.acc-head:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: 2px; border-radius: 8px; }

/* 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) {
  .acc { flex-direction: column; height: auto; }
  .acc-media { display: none; }
  .acc-list-col { padding: 24px clamp(16px, 4vw, 40px) 28px; }
  .acc-heading { font-size: 26px; }
  .acc-title { font-size: 18px; }
  /* 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, 56px) + 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: 16px; padding: 0; }
  .acc-body-text { padding-left: 46px; }
  .acc-body-img {
    display: block;
    width: 100%; max-width: 560px;
    /* 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: 12px;
    background: #E7EBF1; border: 1px solid #D5DCE8;
    box-shadow: inset 0 2px 22px rgba(19, 37, 84, 0.14);
    margin: 4px auto 16px;   /* 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, 370ms) ease-in-out var(--acc-phase, 370ms) 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; }
}

/* ===== 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: 8px; 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: 40;
  background: rgba(8, 15, 34, 0.72);
  display: flex; align-items: center; justify-content: center;
  animation: mvModalIn 240ms cubic-bezier(.2, .7, .3, 1);
}
.mc-intro-modal {
  position: relative;
  width: min(1040px, 92%); height: min(620px, 90%);
  background: #1d3163;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 20px; overflow: hidden;
  display: flex;
}
.mc-intro-panel {
  flex: 1; min-width: 0; padding: 42px 46px 30px;
  display: flex; flex-direction: column; gap: 16px; overflow: auto;
}
.mc-intro-eyebrow {
  display: block;
  font: var(--fw-extrabold, 800) 13px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--signal, #00A3DD);
  margin-bottom: 8px;
}
.mc-intro-item + .mc-intro-item .mc-intro-eyebrow { margin-top: 10px; }
.mc-intro-text { margin: 0; font: 400 20px/1.5 var(--font-ui, 'Archivo', system-ui, sans-serif); color: rgba(255, 255, 255, 0.95); }
.mc-intro-obs {
  display: flex; gap: 12px; align-items: flex-start;
  font: 400 17px/1.5 var(--font-ui, 'Archivo', system-ui, sans-serif); color: rgba(255, 255, 255, 0.88);
}
.mc-intro-dot {
  flex: 0 0 auto; width: 10px; height: 10px; margin-top: 7px; border-radius: 50%;
  border: 2px solid var(--signal, #00A3DD);
}
.mc-intro-item { opacity: 0; transform: translateY(10px); transition: opacity 450ms ease, transform 450ms ease; }
.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, 100px); padding: 15px 40px; cursor: pointer;
  font: 800 16px/1 var(--font-ui, 'Archivo', sans-serif); letter-spacing: 0.3px;
  background: var(--signal, #00A3DD); color: var(--navy, #132554);
}
.mc-intro-btn:hover { background: var(--signal-hover, #008EC1); }
.mc-intro-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(0, 163, 221, 0.55); }
/* 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: #26407a;
  border-left: 1px solid rgba(255, 255, 255, 0.10);
}
.mc-intro-figure::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 22%; pointer-events: none;
  background: linear-gradient(to bottom, #0b1631 0%, #16264f 55%, #1d3163 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.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: 1; filter: drop-shadow(0 10px 22px rgba(0, 0, 0, 0.35));
}
/* 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: 2; pointer-events: none;
  display: flex; align-items: center; justify-content: center;
  background: rgba(127, 29, 29, 0.14);
}
.gs-mark-x svg {
  width: min(38%, 190px); height: auto; overflow: visible;
  animation: gsMarkX 1.9s ease-out both;
  filter: drop-shadow(0 4px 14px rgba(0, 0, 0, 0.35));
}
.gs-mark-x path { stroke: #E0362C; 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: 18px;
  /* 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: 26px 28px calc(13px + var(--mc-intro-btn-row));
  background: #26407a;
}
.mc-intro-modal--speech::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: var(--mc-intro-floor);
  pointer-events: none;
  background: linear-gradient(to bottom, #0b1631 0%, #16264f 55%, #1d3163 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.mc-intro-speech-bubble {
  position: relative; z-index: 1;
  grid-column: 2; grid-row: 1; align-self: start;
  width: 100%;
  background: #fff; color: #16203A; border: 3px solid #0E1C42; border-radius: 15px;
  padding: 13px 18px;
  font: 650 19px/1.35 var(--font-ui, 'Archivo', system-ui, sans-serif);
  box-shadow: 0 6px 16px rgba(8, 12, 26, 0.35); 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: 34px; margin-top: -11px;
  border: 11px solid transparent; border-right-color: #0E1C42;
}
.mc-intro-speech-bubble::before {
  content: ""; position: absolute; right: calc(100% - 4px); top: 34px; margin-top: -8px;
  border: 8px solid transparent; border-right-color: #fff; z-index: 1;
}
.mc-intro-speech-figure {
  position: relative; z-index: 1;
  /* 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 rgba(0, 0, 0, 0.35));
}
/* 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: 2;
  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) {
  /* 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: 26px 24px 20px; }
  .mc-intro-text { font-size: 18px; }
  /* 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: 13px;
  }
  /* `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) {
  /* 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: 1px solid rgba(255, 255, 255, 0.10); }
  .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: 14px;
    --mc-intro-btn-row: 60px;
    padding: 20px 16px 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: 16px; padding: 11px 14px; }
  .mc-intro-speech-bubble::after {
    right: auto; left: 50%; top: 100%; margin: -11px 0 0 -11px;
    border-right-color: transparent; border-top-color: #0E1C42;
  }
  .mc-intro-speech-bubble::before {
    right: auto; left: 50%; top: calc(100% - 4px); margin: -8px 0 0 -8px;
    border-right-color: transparent; border-top-color: #fff;
  }
  .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: 20px; 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(--navy, #132554);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: var(--radius-card, 16px);
  overflow: hidden;
}
.mc-eyebrow {
  font: var(--fw-extrabold, 800) 14px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  letter-spacing: 1.5px; text-transform: uppercase; color: var(--signal, #00A3DD); margin-bottom: 8px;
}

/* 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, 480px); flex-shrink: 0;
  background: var(--surface-deep, #0E1C42);
  border-right: 1px solid rgba(255, 255, 255, 0.10);
  padding: 28px 30px; display: flex; flex-direction: column; gap: 18px; overflow: auto;
}
.mc-scenario-text { font: 400 19px/1.4 var(--font-ui, 'Archivo', system-ui, sans-serif); color: #fff; }
.mc-divider { height: 1px; background: rgba(255, 255, 255, 0.14); }
.mc-obs-list { display: flex; flex-direction: column; gap: 12px; }
.mc-obs { display: flex; align-items: center; gap: 12px; }
.mc-obs-icon { width: 26px; height: 26px; object-fit: contain; flex-shrink: 0; }
.mc-obs-marker {
  width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0;
  border: 2.4px solid var(--signal, #00A3DD);
}
.mc-obs-text { font: 400 17px/1.3 var(--font-ui, 'Archivo', system-ui, sans-serif); color: #fff; }
.mc-scene-wrap { flex: 1; display: flex; align-items: center; justify-content: center; padding: 0 18px; min-width: 0; }
.mc-scene {
  height: 96%; aspect-ratio: 21 / 9; max-width: 100%; flex-shrink: 0; position: relative;
  overflow: hidden; border-radius: 12px; background: #E7EBF1;
  box-shadow: inset 0 2px 22px rgba(19, 37, 84, 0.14); border: 1px solid #D5DCE8;
}
.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) {
  .mc-scene-img { --mc-scene-focal: var(--mc-scene-focal-tablet, var(--mc-scene-focal-desktop, center)); }
}
@media (max-width: 600px) {
  .mc-scene-img { --mc-scene-focal: var(--mc-scene-focal-phone, var(--mc-scene-focal-desktop, center)); }
}
.mc-thought {
  position: absolute; top: 28px; left: 50%; transform: translateX(-50%);
  background: #fff; border: 1.5px solid var(--ink, #16203A); border-radius: 22px;
  padding: 12px 22px; white-space: nowrap;
}
.mc-thought span { font: 700 22px/1 var(--font-ui, 'Archivo', system-ui, sans-serif); color: var(--ink, #16203A); }

/* 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: 48px; padding: 8px 24px;
  display: flex; align-items: center; justify-content: center; background: var(--navy, #132554);
  font: 700 24px/1.25 var(--font-ui, 'Archivo', system-ui, sans-serif); color: #fff; text-align: center;
}

/* Answer cards */
.mc-cards {
  flex: 1 1 40%; min-height: 0; background: var(--navy, #132554);
  /* 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: 20px; padding: 0 48px 16px;
  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) {
  /* 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: 5;
  top: calc((100% - 78px) / 2);            /* centered on the card area above the Submit zone */
  transform: translateY(-50%);
  width: 44px; height: 44px; border-radius: 50%;
  border: 1.5px solid #DCE2EC; background: rgba(255, 255, 255, 0.94);
  color: var(--navy, #132554); font: 700 26px/1 var(--font-ui, 'Archivo', sans-serif);
  cursor: pointer; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
  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 1px;
  transition: opacity 160ms ease;
}
.mc-cards-arrow:hover { background: #fff; }
.mc-cards-arrow:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: 2px; }
.mc-cards-arrow--left { left: 6px; }
.mc-cards-arrow--right { right: 6px; }
.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: 0.42; }
.mc-cards-arrow.is-end:hover { opacity: 0.72; background: #fff; }
@media (max-width: 1024px) {
  .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: 14px;
  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 200ms ease; }
/* 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: 34px 20px 16px;
  background: linear-gradient(to bottom, rgba(14, 28, 66, 0) 0%, rgba(14, 28, 66, 0.72) 38%, rgba(14, 28, 66, 0.92) 100%);
  color: #fff; text-align: left; pointer-events: none;
}
.scy-caption-title {
  font: 800 17px/1.25 var(--font-head, 'Archivo', system-ui, sans-serif);
  color: #fff; margin-bottom: 4px;
}
.scy-caption-body {
  margin: 0; font: 400 14px/1.45 var(--font-ui, 'Archivo', system-ui, sans-serif);
  color: rgba(255, 255, 255, 0.94);
}
/* 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: 3;
  width: 38px; height: 38px; border-radius: 50%;
  border: 1.5px solid #DCE2EC; background: rgba(255, 255, 255, 0.94);
  color: var(--navy, #132554); font: 700 22px/1 var(--font-ui, 'Archivo', sans-serif);
  display: flex; align-items: center; justify-content: center;
  padding: 0 0 1px; cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
  transition: opacity 160ms ease;
}
.scy-arrow:hover:not(:disabled) { background: #fff; }
.scy-arrow:disabled { opacity: 0.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: 3px solid var(--signal, #00A3DD); 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: 8px; justify-content: center; align-items: center; margin-top: 10px; }
.scy-dot {
  width: 9px; height: 9px; padding: 0; border-radius: 50%; cursor: pointer;
  border: 1.5px solid rgba(255, 255, 255, 0.55); background: transparent;
  transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
}
.scy-dot.is-seen { border-color: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.45); }
.scy-dot.is-on { background: var(--signal, #00A3DD); border-color: var(--signal, #00A3DD); transform: scale(1.25); }
.scy-dot:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: 3px; }
@media (max-width: 1024px) {
  /* 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: 26px 14px 12px; }
  .scy-caption-title { font-size: 15px; }
  .scy-caption-body { font-size: 13px; }
  .scy-arrow { width: 34px; height: 34px; font-size: 20px; }
}

.mc-card {
  position: relative; text-align: left; cursor: pointer; background: #fff;
  /* Bottom padding matched to the sides so the picture sits in an even frame (2026-08-05). */
  border: 1.5px solid #DCE2EC; border-radius: 16px; padding: 14px 16px 16px;
  display: flex; flex-direction: column; overflow: hidden;
  transition: border-color 140ms ease, box-shadow 140ms ease;
}
.mc-card.is-selected { border: 2px solid var(--signal, #00A3DD); box-shadow: 0 0 0 4px rgba(0, 163, 221, 0.18); }
.mc-card.is-wrong { border: 2px solid var(--incorrect, #E5463E); box-shadow: 0 0 0 4px rgba(229, 70, 62, 0.15); opacity: 0.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: 0.45; filter: grayscale(0.85); transition: opacity 200ms ease, filter 200ms ease; }
.mc.is-locked-out .mc-card.is-selected { opacity: 0.9; filter: grayscale(0.25); }
.mc-card:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: 2px; }
.mc-card-x {
  position: absolute; top: 14px; right: 14px; width: 26px; height: 26px; border-radius: 50%;
  background: var(--incorrect, #E5463E); color: #fff; font: 800 15px/1 var(--font-ui, system-ui, sans-serif);
  display: flex; align-items: center; justify-content: center;
}
.mc-card-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.mc-card-badge {
  width: 32px; height: 32px; flex-shrink: 0; border-radius: 50%;
  background: var(--navy, #132554); color: #fff;
  font: 800 16px/1 var(--font-ui, system-ui, sans-serif);
  display: flex; align-items: center; justify-content: center;
}
.mc-card.is-selected .mc-card-badge { background: var(--signal, #00A3DD); }
.mc-card.is-wrong .mc-card-badge { background: var(--incorrect, #E5463E); }
.mc-card-title { font: 800 19px/1.2 var(--font-ui, 'Archivo', system-ui, sans-serif); color: var(--ink, #16203A); }
/* 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: 400 15px/1.4 var(--font-ui, 'Archivo', system-ui, sans-serif); color: #5A6577;
  margin-bottom: 10px;
  flex: 1 1 auto;
}
.mc-card-art {
  width: 100%; flex: 1; min-height: 70px; margin-top: 8px; position: relative;
  border-radius: 10px 10px 0 0; overflow: hidden; background: #EEF1F6;
}
/* 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: #EEF1F6; }

/* 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 520ms cubic-bezier(.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: 16px; 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: #fff; border: 2px solid var(--incorrect, #E5463E); opacity: .7;
  padding: 14px 16px;
}
.mc-card-flip-front .mc-card-badge { background: var(--incorrect, #E5463E); }
/* 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: #fff; border: 2px solid var(--incorrect, #E5463E); }
.mc-card-flip-back-head {
  flex: 0 0 auto; background: var(--incorrect, #E5463E); color: #fff; text-align: center;
  padding: 12px 16px; font: 800 15px/1.2 var(--font-ui, 'Archivo', system-ui, sans-serif);
  letter-spacing: .6px; text-transform: uppercase;
}
.mc-card-flip-back-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 14px 16px;
  font: 400 15px/1.4 var(--font-ui, 'Archivo', system-ui, sans-serif); color: var(--ink, #16203A);
}
.mc-card-flip-tryagain {
  flex: 0 0 auto; margin: 0 16px 14px; padding: 10px; border-radius: 100px; border: none;
  cursor: pointer; background: var(--signal, #00A3DD); color: var(--surface-deep, #0E1C42);
  font: 800 14px/1 var(--font-ui, 'Archivo', system-ui, sans-serif); transition: background 140ms ease;
}
.mc-card-flip-tryagain:hover { background: #008ec2; }
@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: 14px; z-index: 20;
  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 18px; 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: 48px; 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: 800 17px/1 var(--font-ui, 'Archivo', system-ui, sans-serif); letter-spacing: 0.5px;
  background: var(--signal, #00A3DD); color: var(--surface-deep, #0E1C42);
  border: none; border-radius: 100px; padding: 15px 48px; cursor: default;
  transition: opacity 320ms cubic-bezier(.2, .7, .3, 1), transform 320ms cubic-bezier(.2, .7, .3, 1), background 140ms ease;
}
.tpl-submit.is-shown { opacity: 1; transform: translateY(0); pointer-events: auto; cursor: pointer; }
.tpl-submit.is-shown:hover { background: #008ec2; }

/* 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: rgba(255, 255, 255, 0.13);
  color: rgba(255, 255, 255, 0.42);
}

/* Feedback modal — a centered overlay over the STAGE only (player bars stay clear) */
.tpl-fb {
  position: absolute; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center;
  padding: 32px 64px; background: rgba(8, 12, 26, 0.8);
}
.tpl-fb-card {
  /* `relative` anchors the dev-only close X (feedback.js), which is absolutely positioned. */
  position: relative;
  width: 100%; max-width: 1360px; max-height: 100%;
  background: #fff; border-radius: 18px; overflow: auto;
  display: flex; flex-direction: column; box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
  animation: mvModalIn 320ms cubic-bezier(.34, 1.56, .64, 1);
}
.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: #26407a; border-left: 1px solid rgba(13, 22, 49, 0.18);
}
.tpl-fb-figure::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 22%;
  pointer-events: none;
  background: linear-gradient(to bottom, #0b1631 0%, #16264f 55%, #1d3163 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.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: 1; filter: drop-shadow(0 10px 22px rgba(0, 0, 0, 0.35));
}
/* 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) {
  /* 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: 1px solid rgba(13, 22, 49, 0.18);
  }
  .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 rgba(0, 0, 0, 0.35));
  }
}

/* Narrow layouts keep the narrator (author 2026-07-27; previously dropped): */
@media (max-width: 1024px) {
  /* tablet: Loretta holds the RIGHT THIRD of the modal */
  .tpl-fb-figure { flex: 0 0 33%; }
}
@media (max-width: 600px) {
  /* 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: 220px;
    border-left: none; border-bottom: 1px solid rgba(13, 22, 49, 0.18);
  }
}
/* 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 250ms ease 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 300ms cubic-bezier(.2, .7, .3, 1); }
.tpl-fb-head { display: flex; align-items: center; gap: 16px; padding: 28px 44px; }
.tpl-fb-head.is-correct { background: var(--correct, #12A66A); }
.tpl-fb-head.is-wrong { background: var(--incorrect, #E5463E); }
.tpl-fb-mark {
  width: 40px; height: 40px; flex-shrink: 0; border-radius: 50%; background: #fff;
  font: 800 21px/1 var(--font-ui, system-ui, sans-serif);
  display: flex; align-items: center; justify-content: center;
}
.tpl-fb-head.is-correct .tpl-fb-mark { color: var(--correct, #12A66A); }
.tpl-fb-head.is-wrong .tpl-fb-mark { color: var(--incorrect, #E5463E); }
.tpl-fb-title { font: 800 26px/1.2 var(--font-ui, 'Archivo', system-ui, sans-serif); color: #fff; text-transform: uppercase; letter-spacing: 0.5px; }
.tpl-fb-body { flex: 1; display: flex; flex-direction: column; padding: 40px 44px; }
.tpl-fb-text { position: relative; font: 400 23px/1.55 var(--font-ui, 'Archivo', system-ui, sans-serif); color: var(--ink, #16203A); margin: 0 0 26px; }
.tpl-fb-takeaway {
  background: rgba(0, 163, 221, 0.06); border: 1px solid rgba(0, 163, 221, 0.25); border-radius: 12px;
  padding: 20px 24px; margin-bottom: 34px;
  font: 400 19px/1.5 var(--font-ui, 'Archivo', system-ui, sans-serif); color: var(--ink, #16203A);
}
.tpl-fb-takeaway b { color: var(--signal, #00A3DD); font-weight: 800; }
/* 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. */
.tpl-fb-actions { display: flex; justify-content: center; margin-top: auto; }
.tpl-fb-btn {
  font: 800 17px/1 var(--font-ui, 'Archivo', system-ui, sans-serif); letter-spacing: 0.5px;
  background: var(--signal, #00A3DD); color: var(--surface-deep, #0E1C42);
  border: none; border-radius: 100px; padding: 15px 44px; cursor: pointer;
  transition: background 140ms ease;
}
.tpl-fb-btn:hover { background: #008ec2; }

@media (prefers-reduced-motion: reduce) {
  .tpl-submit { transition: opacity 0ms; }
  .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) {
  .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: 14px; padding: 20px 18px; }
  .mc-scene-wrap { padding: 16px; }
  /* 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: 20px; padding: 12px 16px; }
  .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: 14px; padding: 16px; scrollbar-width: none;
  }
  .mc-cards::-webkit-scrollbar { display: none; }
  .mc-card { flex: 0 0 44%; scroll-snap-align: start; min-height: 220px; }   /* ~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: 12px; margin-top: 6px; }
  .tpl-fb { position: fixed; padding: 16px; }
  .tpl-fb-card { max-height: calc(100dvh - 32px); }
  .tpl-fb-head { padding: 20px 24px; gap: 12px; }
  .tpl-fb-mark { width: 32px; height: 32px; font-size: 17px; }
  .tpl-fb-title { font-size: 20px; }
  .tpl-fb-body { padding: 24px; }
  .tpl-fb-text { font-size: 18px; margin-bottom: 20px; }
  .tpl-fb-takeaway { font-size: 16px; padding: 16px 18px; margin-bottom: 24px; }
  .tpl-fb-btn { padding: 14px 32px; }
}
@media (max-width: 600px) {
  /* 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: 12px; padding: 12px 16px 16px; }
  /* Compact the scenario so it (text + observations) stays in view above the carousel. */
  .mc-rail { padding: 14px 16px; gap: 10px; }
  .mc-obs-list { gap: 8px; }
  .mc-scenario-text { font-size: 16px; }
  .mc-obs-text { font-size: 15px; }
  .mc-scene-wrap { padding: 8px 16px; }
  /* 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: 17px; padding: 8px 14px; }
}

/* ===== 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: 8px; }

/* checkbox marker on each card (top-right) — the select-all affordance */
.mc-card-check {
  position: absolute; top: 14px; right: 14px; width: 24px; height: 24px; flex-shrink: 0;
  border-radius: 6px; border: 2px solid #B7C0CE; background: #fff;
  display: flex; align-items: center; justify-content: center;
  font: 800 14px/1 var(--font-ui, system-ui, sans-serif); color: #fff;
}
.mc-card.is-selected .mc-card-check { background: var(--signal, #00A3DD); border-color: var(--signal, #00A3DD); }
/* keep the card title clear of the checkbox */
.tpl-page--multi-select .mc-card-head { padding-right: 30px; }

/* the "select all that apply" instruction — inline on the prompt line (lighter + smaller) */
.mc-prompt-inst { color: rgba(255, 255, 255, 0.62); font-weight: 500; font-size: 20px; margin-left: 12px; }

@media (max-width: 1024px) {
  .tpl-page--multi-select { padding: 0; }
  .mc-prompt-inst { font-size: 15px; margin-left: 8px; }
}

/* 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) {
  .mc-prompt-row { flex-direction: column; align-items: stretch; gap: 8px; }
  .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: 4px; 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, #1A2E60);
}
.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: #fff; stroke-width: 11; stroke-linecap: round;
  filter: blur(4px); opacity: 0;
  transform-box: fill-box; transform-origin: 50% 100%;
  animation: wel-wisp-rise 6s ease-in-out infinite;
}
.wel-wisp--a { --wisp-peak: 0.22; animation-duration: 5.6s; }
.wel-wisp--b { --wisp-peak: 0.16; animation-duration: 7.2s; animation-delay: -2.8s; }
@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: 0.06em; text-transform: uppercase; color: var(--signal, #00A3DD);
  font-family: var(--font-ui, 'Archivo', system-ui, sans-serif); font-weight: 800;
}
.wel-title {
  font-family: var(--font-display, 'Archivo', sans-serif); font-weight: 900;
  color: #fff; letter-spacing: -0.01em;
}
.wel-subtitle {
  font-family: var(--font-ui, 'Archivo', system-ui, sans-serif); font-weight: 500;
  color: rgba(255, 255, 255, 0.9);
}

/* 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) {
  /* 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, 56px));
    background: var(--surface-deep, #0E1C42);
  }
  /* 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: 3px solid var(--signal, #00A3DD); border-radius: 0;
    background: var(--surface-deep, #0E1C42); 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: clamp(14px, 4.8cqw, 30px); line-height: 1; margin: 0; }
  .wel-title { font-size: clamp(24px, 8cqw, 56px); line-height: 1.05; margin: 0; }
  .wel-subtitle { font-size: clamp(15px, 5.4cqw, 34px); line-height: 1.32; 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: 8px; }
.scn {
  position: relative; height: 100%; min-height: 0;
  display: grid; grid-template-columns: 1.15fr 1fr;
  background: var(--navy, #132554);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: var(--radius-card, 16px); overflow: hidden;
}
.scn-scene { position: relative; overflow: hidden; background: var(--surface-deep, #0E1C42); }
.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: 10px;
  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: 6px; }
.scn-obs-hint {
  position: absolute; top: 16px; left: 16px; z-index: 3;
  display: flex; align-items: center; gap: 10px;
  background: rgba(14, 28, 66, 0.86); border-radius: 100px; padding: 9px 16px;
  font: 600 15px/1 var(--font-ui, 'Archivo', system-ui, sans-serif); color: #fff;
}
.scn-obs-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--signal, #00A3DD); animation: mvTwinkle 1.6s ease-in-out infinite; }
.scn-obs-hint.is-done .scn-obs-dot { background: var(--correct, #12A66A); 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) {
  .tpl-page--click-reveal .scn-scene.has-obs {
    display: grid; grid-template-columns: minmax(260px, 30%) 1fr;
    align-items: center; gap: 28px; background: var(--navy, #132554); padding: 22px;
  }
  .tpl-page--click-reveal .scn-obs-hint {
    position: static; z-index: auto; display: block;
    background: none; border-radius: 0; padding: 0;
    font: 400 17px/1.5 var(--font-ui, 'Archivo', system-ui, sans-serif);
    color: rgba(255, 255, 255, 0.92);
  }
  .tpl-page--click-reveal .scn-obs-hint::before {
    content: "Instructions"; display: block; margin-bottom: 8px;
    font: 800 15px/1.2 var(--font-head, 'Archivo', sans-serif);
    letter-spacing: 1.3px; text-transform: uppercase; color: var(--signal, #00A3DD);
  }
  .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: 14px; }
  /* 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: 12px; overflow: hidden; }

  /* 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: 22px;
  }
  .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: 1px solid rgba(255, 255, 255, 0.16); padding-top: 18px;
  }
  .tpl-page--click-reveal .scn-callout-sub {
    font-size: 19px; color: var(--signal, #00A3DD); margin-right: 0;
  }
  .tpl-page--click-reveal .scn-callout-note { font-size: 18px; line-height: 1.55; color: rgba(255,255,255,0.92); font-weight: 400; }
  .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: 18px; }
  .tpl-page--click-reveal .scn-obs-count { grid-row: 3; align-self: start; }
}

.scn-obs-count {
  position: absolute; top: 16px; right: 16px; z-index: 3;
  background: rgba(14, 28, 66, 0.86); border-radius: 100px; padding: 9px 16px;
  font: 800 14px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  letter-spacing: 0.5px; text-transform: uppercase; color: var(--signal, #00A3DD);
}
.scn-obs-count.is-done { color: var(--correct, #12A66A); }
.scn-hot {
  position: absolute; transform: translate(-50%, -50%);
  width: 44px; height: 44px; border: none; background: transparent; padding: 0; cursor: pointer; z-index: 2;
}
.scn-hot-pulse { position: absolute; inset: 4px; border-radius: 50%; background: rgba(0, 163, 221, 0.55); animation: mvPulse 2s ease-out infinite; }
.scn-hot-dot {
  position: absolute; inset: 13px; border-radius: 50%;
  background: var(--signal, #00A3DD); border: 2px solid #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
  display: flex; align-items: center; justify-content: center; color: #fff;
  font: 800 12px/1 var(--font-ui, system-ui, sans-serif); animation: mvTwinkle 1.8s ease-in-out infinite;
}
.scn-hot.is-seen .scn-hot-dot { background: var(--correct, #12A66A); animation: none; }
.scn-hot:focus-visible { outline: 3px solid #fff; outline-offset: 2px; border-radius: 50%; }
/* 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: 70px; height: 70px; }
.tpl-page--click-reveal:not(.is-responsive) .scn-hot-pulse { inset: 6px; }
.tpl-page--click-reveal:not(.is-responsive) .scn-hot-dot { inset: 21px; font-size: 19px; }
/* 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: 5; pointer-events: none;
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.scn-callout.is-open { background: rgba(8, 12, 26, 0.55); 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: 800 17px/1.25 var(--font-ui, 'Archivo', sans-serif);
  color: #26407a; margin: 2px 24px 8px 0;
}
.cr-m-desc-sub {
  font: 800 15px/1.25 var(--font-ui, 'Archivo', sans-serif);
  color: #A9C2F2; margin: 0 0 6px;
}
.scn-callout-card {
  position: relative; width: min(360px, 84%); max-height: 88%; overflow: auto;
  background: #fff; border: 1.5px solid var(--ink, #16203A); border-radius: 14px;
  padding: 20px 22px; box-shadow: 0 18px 44px rgba(0, 0, 0, 0.4);
  animation: mvPopIn 220ms cubic-bezier(.2, .7, .3, 1);
}
.scn-callout-note { font: 600 17px/1.5 var(--font-ui, 'Archivo', system-ui, sans-serif); color: var(--ink, #16203A); }
.scn-callout-close {
  position: absolute; top: 6px; right: 8px; width: 30px; height: 30px;
  border: none; background: transparent; color: #8A93A3; font-size: 22px; line-height: 1;
  cursor: pointer; border-radius: 8px;
}
.scn-callout-close:hover { color: var(--ink, #16203A); background: rgba(0, 0, 0, 0.05); }

/* locked options + hint (until all evidence reviewed) */
.scn-lock-hint {
  display: flex; align-items: center; gap: 8px;
  font: 600 15px/1.4 var(--font-ui, 'Archivo', system-ui, sans-serif); color: rgba(255, 255, 255, 0.72);
}
.scn-lock-ico { font-size: 15px; }
.scn-list.is-locked { opacity: 0.45; pointer-events: none; filter: saturate(0.6); }
.scn-choice {
  background: var(--navy, #132554); border-left: 1px solid rgba(255, 255, 255, 0.10);
  padding: 44px 52px; display: flex; flex-direction: column; gap: 16px; overflow: auto;
}
.scn-eyebrow {
  font: var(--fw-extrabold, 800) 16px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  letter-spacing: 1.5px; text-transform: uppercase; color: var(--signal, #00A3DD);
}
.scn-question {
  font-family: var(--font-display, 'Archivo', sans-serif); font-weight: 800; font-size: 34px;
  color: #fff; margin: 2px 0 8px; line-height: 1.2;
}
.scn-list { display: flex; flex-direction: column; gap: 13px; }

/* option row — the AnswerOption RADIO primitive (round marker), reproduced from tokens */
.scn-opt {
  display: flex; align-items: center; gap: 14px;
  border: 1.5px solid #DCE2EC; background: #fff; border-radius: 12px;
  padding: 14px 16px; min-height: 44px; cursor: pointer;
  transition: border-color 120ms cubic-bezier(.2, .7, .3, 1), background 120ms;
}
/* 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: 2px solid var(--signal, #00A3DD); background: #F2F9FE; }
.scn-opt.is-correct   { border: 2px solid var(--correct, #12A66A); background: #E6F6EF; }
.scn-opt.is-incorrect { border: 2px solid var(--incorrect, #E5463E); background: #FCEBEA; }
.scn-opt[tabindex="-1"] { cursor: default; }
.scn-opt:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: 2px; }
.scn-badge {
  width: 30px; height: 30px; flex-shrink: 0; border-radius: 6px;
  background: #EEF1F6; color: var(--navy, #132554);
  font: 800 14px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  display: flex; align-items: center; justify-content: center;
}
.scn-text { flex: 1; font: 400 16px/1.35 var(--font-ui, 'Archivo', system-ui, sans-serif); color: var(--ink, #16203A); }
.scn-opt.is-selected .scn-text { color: var(--signal, #00A3DD); font-weight: 700; }
.scn-opt.is-correct .scn-text { font-weight: 600; }
.scn-opt.is-incorrect .scn-text { font-weight: 500; }
.scn-marker {
  width: 22px; height: 22px; flex-shrink: 0; border-radius: 50%;    /* round = single-select */
  border: 2px solid #B7C0CE; display: flex; align-items: center; justify-content: center;
  font: 800 13px/1 var(--font-ui, system-ui, sans-serif); color: #fff;
}
.scn-opt.is-selected .scn-marker { border-color: var(--signal, #00A3DD); }
.scn-marker-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--signal, #00A3DD); }
.scn-opt.is-correct .scn-marker { background: var(--correct, #12A66A); border-color: var(--correct, #12A66A); }
.scn-opt.is-incorrect .scn-marker { background: var(--incorrect, #E5463E); border-color: var(--incorrect, #E5463E); }

/* inline feedback panel (beneath the options) */
.scn-fb {
  margin-top: 10px; display: flex; gap: 14px; align-items: flex-start;
  border: 2px solid; border-radius: 12px; padding: 16px 18px;
  animation: mvModalIn 260ms cubic-bezier(.2, .7, .3, 1);
}
.scn-fb.is-correct { border-color: var(--correct, #12A66A); background: rgba(18, 166, 106, 0.12); }
.scn-fb.is-wrong { border-color: var(--incorrect, #E5463E); background: rgba(229, 70, 62, 0.12); }
.scn-fb-mark {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: 50%; color: #fff;
  font: 800 17px/1 var(--font-ui, system-ui, sans-serif);
  display: flex; align-items: center; justify-content: center;
}
.scn-fb.is-correct .scn-fb-mark { background: var(--correct, #12A66A); }
.scn-fb.is-wrong .scn-fb-mark { background: var(--incorrect, #E5463E); }
.scn-fb-title { font: 800 19px/1.2 var(--font-ui, 'Archivo', system-ui, sans-serif); color: #fff; margin-bottom: 3px; }
.scn-fb-body { font: 400 16px/1.5 var(--font-ui, 'Archivo', system-ui, sans-serif); color: rgba(255, 255, 255, 0.82); }

@media (max-width: 1024px) {
  .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: 24px clamp(16px, 4vw, 40px) 40px; overflow: visible; }
  .scn-question { font-size: 26px; }
}
@media (max-width: 600px) {
  .scn-question { font-size: 22px; }
  .scn-text { font-size: 15px; }
}

/* ===== 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) {
  .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, 56px) - 96px);
    background: var(--surface-deep, #0E1C42);
  }
  .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: 18px clamp(16px, 5vw, 28px) 8px; }
  .scn-m-intro { margin: 0; font: 800 21px/1.3 var(--font-ui, 'Archivo', sans-serif); color: #fff; letter-spacing: -0.2px; }

  /* the vertical evidence image (two stacked scenes), aspect-locked portrait, numbered markers on it */
  .scn-m-scene {
    position: relative; width: 100%; max-width: 460px; margin: 12px auto 0;
    overflow: hidden; border-radius: 12px; background: var(--surface-deep, #0E1C42);
    border: 1px solid rgba(255, 255, 255, 0.10);
  }
  /* 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: 14px; padding: 16px 16px 6px;
  }

  /* decision block, below the numbered dots */
  .scn-m-decide { display: flex; flex-direction: column; padding: 4px clamp(16px, 5vw, 28px) 28px; }
  .scn-m-question { font: 800 22px/1.25 var(--font-ui, 'Archivo', sans-serif); color: #fff; margin: 8px 0 16px; letter-spacing: -0.2px; }
  /* 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: 460px; margin: 10px auto 0; border-radius: 12px;
    border: 1px solid rgba(255, 255, 255, 0.10);
  }
  .scn-m-options { display: flex; flex-direction: column; gap: 12px; }
  .scn-m-options-lock {
    display: flex; align-items: center; gap: 10px; padding: 22px 18px;
    background: rgba(255, 255, 255, 0.04); border: 1px dashed rgba(255, 255, 255, 0.18); border-radius: 12px;
    font: 600 15px/1.4 var(--font-ui, 'Archivo', sans-serif); color: rgba(255, 255, 255, 0.60);
  }
  .scn-m-lock-ico { font-size: 18px; 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: 8px; position: relative; }   /* anchors the outro's floating Continue + modal */
.tpl-page--click-reveal .scn-scene {
  height: 100%; border-radius: var(--radius-card, 16px); border: 1px solid rgba(255, 255, 255, 0.10);
}

@media (max-width: 1024px) {
  /* ===== 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, 56px));
    padding-bottom: calc(70px + env(safe-area-inset-bottom, 0px));
    background: var(--surface-deep, #0E1C42);
  }
  .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(--navy, #132554); color: #fff;
    padding: 16px clamp(16px, 5vw, 28px) 18px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.10);
  }
  .cr-m-instructions-title {
    font: 800 13px/1.2 var(--font-head, 'Archivo', sans-serif);
    letter-spacing: 1.2px; text-transform: uppercase;
    color: var(--signal, #00A3DD); margin-bottom: 6px;
  }
  .cr-m-instructions-body {
    margin: 0; font: 400 16px/1.45 var(--font-ui, 'Archivo', sans-serif);
    color: rgba(255, 255, 255, 0.92);
  }

  .cr-m-desc {
    flex: 1 1 auto; background: #26407a; color: #fff; min-height: 96px;
    padding: 16px clamp(16px, 5vw, 28px) 18px;
    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: 1px solid rgba(255, 255, 255, 0.10);
  }
  .cr-m-desc-live { animation: mvCrPop 320ms var(--ease-overshoot, cubic-bezier(.34, 1.56, .64, 1)); }
  .cr-m-desc-eyebrow {
    display: flex; align-items: center; gap: 9px; margin-bottom: 8px;
    font: 800 13px/1 var(--font-ui, 'Archivo', sans-serif);
    letter-spacing: 1px; text-transform: uppercase; color: var(--signal, #00A3DD);
  }
  .cr-m-desc-num {
    width: 22px; height: 22px; flex-shrink: 0; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--signal, #00A3DD); color: var(--navy, #132554); font: 800 12px/1 var(--font-ui, sans-serif);
  }
  .cr-m-desc-note { font: 500 16px/1.5 var(--font-ui, 'Archivo', sans-serif); color: rgba(255, 255, 255, 0.92); }
  .cr-m-hint { font: 600 15px/1.5 var(--font-ui, 'Archivo', sans-serif); color: rgba(255, 255, 255, 0.70); text-align: center; }

  /* 2) picture-in-picture — the signal-blue comic-strip halftone material */
  .cr-m-pip {
    flex: 0 0 auto; padding: 14px clamp(16px, 5vw, 28px);
    display: flex; align-items: center; justify-content: center;
    background:
      radial-gradient(rgba(255, 255, 255, 0.16) 1.5px, transparent 1.7px) 0 0 / 13px 13px,
      radial-gradient(130% 95% at 50% 120%, #0089c4 0%, #00A3DD 55%, #23b6e8 100%);
    border-bottom: 4px solid #0E1C42;
  }
  /* Row so the badge sits BESIDE the frame, not overlaid on the cutout art. */
  .cr-m-pip-row { display: flex; align-items: center; gap: 10px; width: 100%; max-width: 420px; }
  .cr-m-pip-frame, .cr-m-pip-empty {
    position: relative; flex: 1 1 auto; min-width: 0; aspect-ratio: 16 / 10;
    border-radius: 12px; overflow: hidden; border: 3px solid #fff;
    box-shadow: 0 6px 18px rgba(8, 18, 40, 0.35);
  }
  .cr-m-pip-frame { animation: mvCrPop 340ms var(--ease-overshoot, cubic-bezier(.34, 1.56, .64, 1)); }
  .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: 12px;
    background: rgba(10, 18, 40, 0.35); border-style: dashed; border-color: rgba(255, 255, 255, 0.70);
    color: rgba(255, 255, 255, 0.90); font: 600 14px/1.4 var(--font-ui, 'Archivo', sans-serif);
  }
  .cr-m-pip-badge {
    flex: 0 0 auto; width: 28px; height: 28px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: var(--signal, #00A3DD); color: var(--navy, #132554);
    font: 800 15px/1 var(--font-ui, 'Archivo', sans-serif);
    border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
  }

  /* 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(--surface-deep, #0E1C42);
  }
  .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: 30px; height: 30px; border-radius: 50%; padding: 0; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font: 800 14px/1 var(--font-ui, 'Archivo', sans-serif);
    background: rgba(19, 37, 84, 0.82); color: #fff; border: 2px solid #fff;
    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.40);
  }
  .cr-m-marker.is-seen { background: rgba(18, 166, 106, 0.90); }
  .cr-m-marker.is-active {
    background: var(--signal, #00A3DD); color: var(--navy, #132554);
    transform: translate(-50%, -50%) scale(1.18);
    box-shadow: 0 0 0 5px rgba(0, 163, 221, 0.35), 0 1px 6px rgba(0, 0, 0, 0.45);
  }

  /* 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: 10px; padding: 16px 16px 8px;
  }
  .cr-m-dots-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 16px; }
  .cr-m-dot {
    width: 46px; height: 46px; border-radius: 50%; padding: 0; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    font: 800 16px/1 var(--font-ui, 'Archivo', sans-serif);
    background: #fff; color: #8A93A3; border: 1.5px solid var(--border, #DCE2EC);
    transition: transform 120ms ease;
  }
  .cr-m-dot.is-seen { color: var(--correct, #12A66A); border-color: var(--correct, #12A66A); background: rgba(18, 166, 106, 0.12); }
  .cr-m-dot.is-current {
    background: var(--signal, #00A3DD); color: var(--navy, #132554);
    border: 2px solid #0E1C42; box-shadow: 0 0 0 4px rgba(0, 163, 221, 0.22); transform: scale(1.06);
  }
  .cr-m-dot:focus-visible, .cr-m-marker:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(0, 163, 221, 0.55); }

  /* (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) {
  /* Tighten the stack so the whole 3-over-2 dot cluster clears the fold on a phone. */
  .cr-m-desc { min-height: 82px; padding-top: 12px; padding-bottom: 12px; }
  .cr-m-desc-note { font-size: 15px; }
  .cr-m-pip { padding-top: 10px; padding-bottom: 10px; }
  .cr-m-pip-row { max-width: 300px; }
  .cr-m-scene { max-height: 33vh; }
  .cr-m-dots { padding: 12px 16px 6px; gap: 8px; }
  .cr-m-dots-row { gap: 13px; }
  .cr-m-dot { width: 42px; height: 42px; font-size: 15px; }
}
@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: 28px 36px;
  max-width: 640px;
  background: #fff;
  color: #B3261E;
  border: 2px solid #B3261E;
  border-radius: 12px;
  font: 600 16px/1.5 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: 20px; padding: 20px; box-sizing: border-box; }
.obs-stage {
  flex: 1 1 auto; min-width: 0; position: relative;
  background: #0e1830; border-radius: var(--radius-lg, 16px); 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: 5; cursor: pointer; text-align: left;
  -webkit-appearance: none; appearance: none;
  background: #fff; color: #16203A; border: 3px solid #0E1C42; border-radius: 16px;
  padding: 14px 18px 12px; box-shadow: 0 12px 34px rgba(8,12,26,.45);
  display: flex; flex-direction: column; gap: 2px;
  animation: obs-gate-in 320ms cubic-bezier(.2,.7,.3,1);
}
.obs-gate::after { content: ""; position: absolute; top: 100%; left: 40px; width: 0; height: 0; border: 12px solid transparent; border-top-color: #0E1C42; }
.obs-gate-speaker { font: 800 12px/1 var(--font-head, "Archivo", sans-serif); letter-spacing: 1px; text-transform: uppercase; color: var(--signal); }
.obs-gate-text { font: 700 clamp(16px, 1.8vw, 20px)/1.35 system-ui, sans-serif; }
.obs-gate-hint { margin-top: 6px; font: 700 11px/1 var(--font-head, "Archivo", sans-serif); letter-spacing: 1px; text-transform: uppercase; color: var(--muted, #6A7590); }
.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: 28px 20px 16px;
  background: linear-gradient(to top, rgba(10,16,32,0.92), rgba(10,16,32,0));
  pointer-events: none;
}
.obs-panel { flex: 0 0 min(340px, 34%); display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
.obs-eyebrow {
  font: 800 12px/1 var(--font-head, "Archivo", sans-serif); letter-spacing: 1.5px;
  text-transform: uppercase; color: var(--signal);
}
.obs-heading { margin: 0; font: 800 22px/1.2 var(--font-head, "Archivo", sans-serif); color: var(--navy); letter-spacing: -0.3px; }

/* ---- the transcript (shared desktop caption + responsive comic copy) ---- */
.obs-transcript { margin: 0; font: 600 clamp(16px, 1.5vw, 20px)/1.5 system-ui, sans-serif; color: #fff; }
.obs-caption .obs-transcript { text-shadow: 0 1px 3px rgba(0,0,0,0.5); }
.obs-transcript-name { display: block; font: 800 12px/1 var(--font-head, "Archivo", sans-serif); letter-spacing: 1px; text-transform: uppercase; color: var(--signal); margin-bottom: 4px; }
.obs-caret {
  display: inline-block; width: 2px; height: 1em; margin-left: 2px; vertical-align: -0.15em;
  background: currentColor; animation: obs-blink 1s step-end infinite;
}
@keyframes obs-blink { 50% { opacity: 0; } }

/* ---- evidence: always-visible, selectable observations ---- */
.obs-evidence { background: #fff; border: 1px solid var(--line, #DCE2EC); border-radius: 14px; padding: 16px 18px; }
.obs-desktop .obs-evidence { flex: 0 1 auto; }
.obs-evidence-title { font: 800 12px/1 var(--font-head, "Archivo", sans-serif); letter-spacing: 1px; text-transform: uppercase; color: var(--navy); margin-bottom: 10px; }
.obs-evidence-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.obs-evidence-item { padding-left: 16px; position: relative; font: 400 15px/1.5 system-ui, sans-serif; color: var(--ink); }
.obs-evidence-item::before { content: ""; position: absolute; left: 0; top: 0.62em; width: 7px; height: 7px; border-radius: 50%; background: var(--signal); }
.obs-evidence-label { font-weight: 700; color: var(--navy); }
.obs-evidence-label::after { content: " — "; color: var(--muted, #6A7590); font-weight: 400; }

/* ---- 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: 16px; padding: 16px; box-sizing: border-box;
  height: calc(100dvh - var(--rc-header-h, 56px) - 96px);
}
.obs-side {
  flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 10px;
  border-radius: var(--radius-lg, 16px); padding: 14px 14px 10px; box-sizing: border-box; overflow: hidden;
  border: 3px solid #0E1C42;
  background:
    radial-gradient(rgba(255,255,255,0.16) 1.5px, transparent 1.7px) 0 0 / 13px 13px,
    radial-gradient(130% 95% at 50% 120%, #0089c4 0%, #00A3DD 55%, #23b6e8 100%);
}
.obs-side-label {
  flex: 0 0 auto; font: 800 12px/1 var(--font-head, "Archivo", sans-serif);
  letter-spacing: 1px; text-transform: uppercase; color: #fff; text-shadow: 0 1px 0 #0E1C42;
}
.obs-side-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 10px; }
/* 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: 3px; flex: 0 0 auto;
  background: #fff; color: #16203A; border: 3px solid #0E1C42; border-radius: 14px;
  padding: 12px 14px; box-shadow: 0 8px 22px rgba(8,12,26,.4);
  animation: obs-side-gate-in 480ms cubic-bezier(.2,.7,.3,1) 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, #00A3DD); box-shadow: 0 0 0 3px rgba(0,163,221,.45); 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: 8px; }
.obs-side-speech {
  display: flex; flex-direction: column; gap: 2px;
  background: #fff; color: #16203A; border: 3px solid #0E1C42; border-radius: 14px;
  padding: 10px 14px; box-shadow: 0 4px 12px rgba(8,12,26,.3);
}
.obs-side-speech-name { font: 800 11px/1 var(--font-head, "Archivo", sans-serif); letter-spacing: 1px; text-transform: uppercase; color: var(--signal, #00A3DD); }
.obs-side-speech-text { font: 600 clamp(14px, 1.4vw, 17px)/1.32 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: 8px; }
.obs-side-scroll > .obs-side-log { margin-top: auto; }
.obs-hist-line {
  margin: 0; background: #fff; color: #16203A; border: 3px solid #0E1C42; border-radius: 14px;
  padding: 8px 12px; font: 600 clamp(13px, 1vw, 16px)/1.4 system-ui, sans-serif;
  box-shadow: 0 4px 12px rgba(8,12,26,.3);
}
/* 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: #0a1533; border: 1px solid rgba(255,255,255,0.14);   /* OPAQUE navy — solid contrast for the signal-blue title + white body (a11y) */
  border-radius: 14px; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px;
}
/* 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: 2px; }
.obs-side-evidence .obs-evidence-item { color: #eaf6ff; opacity: 0; transition: opacity .6s ease; }
.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: #fff; }
.obs-side-evidence .obs-evidence-label { color: #fff; }
.obs-side-evidence .obs-evidence-label::after { color: rgba(255,255,255,.7); }
/* the "What we can see" TITLE: signal blue and larger than the body text */
.obs-side-card .obs-side-label { color: var(--signal, #00A3DD); font-size: clamp(15px, 1.4vw, 18px); }
/* 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 .75s ease; }
.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: 4px; padding-bottom: 2px;
}
.obs-side-head .ppt-comic-head { height: calc(100% - 24px); width: auto; max-width: 100%; }
.obs-side-head .ppt-comic-name {
  font: 800 clamp(11px, 1vw, 14px)/1 var(--font-head, "Archivo", sans-serif); letter-spacing: .5px;
  text-transform: uppercase; color: #fff; text-shadow: 0 1px 0 #0E1C42, 0 2px 4px rgba(8,12,26,.4);
}
/* 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-lg, 16px);
  background: #0e1830; border: 3px solid #0E1C42;
}

/* ---- 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, 56px) - 96px); }
/* 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, 56px) - 96px) * 0.45);
  padding-bottom: 1.1vh;
  position: sticky; top: 0; z-index: 6;
}
/* 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, #00A3DD); box-shadow: 0 0 0 3px rgba(0,163,221,.45); outline: none; }
.obs-comic-gate .obs-gate-hint { display: block; margin-top: 4px; font-size: .82em; font-weight: 700; color: var(--signal, #00A3DD); text-transform: uppercase; letter-spacing: .4px; }
/* 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: #0e1830;
  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: 39; background: rgba(10, 16, 35, 0.42); }
.obs-ev-panel {
  position: fixed; z-index: 40; top: var(--rc-header-h, 56px); bottom: 0; right: 0;
  width: min(52vw, 420px);                      /* tablet: same as the References/Menu drawer */
  transform: translateX(100%);
  transition: transform 240ms cubic-bezier(.2, .7, .3, 1);
  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) { .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: -44px; top: calc((100dvh - var(--rc-header-h, 56px) - 96px) * 0.45 + 12px);
  width: 44px; margin: 0; border: 0; padding: 16px 8px;
  /* Deep Navy on Signal Blue = 5.13:1 (white would fail WCAG 1.4.3/1.4.11) */
  background: var(--signal, #00A3DD); color: var(--navy, #132554);
  font: 700 13px/1 system-ui, sans-serif; letter-spacing: .02em;
  writing-mode: vertical-rl; text-orientation: mixed;
  border-radius: 10px 0 0 10px; box-shadow: -3px 0 10px rgba(0,0,0,.28);
  cursor: pointer; pointer-events: auto;
}
.obs-ev-tab:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
.obs-ev-drawer {
  height: 100%; overflow-y: auto; pointer-events: auto;
  background: var(--white, #fff); box-shadow: -8px 0 24px rgba(10, 16, 35, 0.35); padding: 18px 18px 24px;
}
.obs-ev-drawer-head { margin: 0 0 12px; font: 800 18px/1.2 var(--font-head, "Archivo", sans-serif); color: var(--navy, #132554); }
.obs-ev-drawer-head:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: 3px; border-radius: 4px; }
.obs-ev-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.obs-ev-item { font: 400 15px/1.5 system-ui, sans-serif; color: var(--ink, #16203A); padding-left: 14px; position: relative; }
.obs-ev-item::before { content: ""; position: absolute; left: 0; top: .62em; width: 7px; height: 7px; border-radius: 50%; background: var(--signal, #00A3DD); }
.obs-ev-item-name { font-weight: 700; color: var(--navy, #132554); }
.obs-ev-item-name::after { content: " — "; color: var(--muted, #6A7590); font-weight: 400; }
@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(--navy, #132554);
  text-align: center;
}
.cover-stack { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.cover-eyebrow {
  font: 800 clamp(14px, 1.6vw, 18px)/1 var(--font-ui, 'Archivo', sans-serif);
  letter-spacing: 3px; text-transform: uppercase;
  color: var(--signal, #00A3DD);
}
.cover-title {
  margin: 0;
  font: 800 clamp(34px, 5.5vw, 72px)/1.1 var(--font-ui, 'Archivo', sans-serif);
  letter-spacing: -1px; text-transform: uppercase;
  color: #fff;
}
.cover-badge {
  margin-top: 10px;
  font: 800 clamp(18px, 2.4vw, 28px)/1 var(--font-ui, 'Archivo', sans-serif);
  letter-spacing: 4px; text-transform: uppercase;
  color: var(--navy, #132554);
  background: var(--signal, #00A3DD);
  border-radius: var(--radius-pill, 100px);
  padding: 14px 36px;
}

/* ===== 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: #26407a;                                 /* back wall — light navy */
}
.lacc::after {                                         /* the floor */
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 24%; pointer-events: none; z-index: 0;
  background: linear-gradient(to bottom, #0b1631 0%, #16264f 55%, #1d3163 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); /* 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: 1; }

/* --- 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: 180px;
  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 8px 7%;
  background: transparent;              /* the room (wall + floor) shows through */
  border-right: 1px solid rgba(255, 255, 255, 0.08);
}
.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 rgba(0, 0, 0, 0.35));
}

/* --- RIGHT: the compact accordion column --- */
.lacc-list-col {
  flex: 1;
  min-width: 0;
  padding: 28px 40px 30px;
  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: 860px; margin-left: auto; margin-right: auto;
}
.lacc-heading { margin-top: 5px; margin-bottom: 14px; }
.lacc-eyebrow {
  font: var(--fw-extrabold, 800) 15px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  letter-spacing: 1.5px; text-transform: uppercase; color: var(--signal, #00A3DD);
}
.lacc-heading {
  font-family: var(--font-display, 'Archivo', sans-serif);
  font-weight: 800; font-size: 30px; line-height: 1.15;
  color: #fff; margin: 5px auto 14px;   /* auto sides keep the centered-group margins */
}
.lacc-list { display: flex; flex-direction: column; }
.lacc-item { border-bottom: 1px solid rgba(255, 255, 255, 0.12); }

/* 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: 10px 4px; display: flex; align-items: center; gap: 12px;
}
.lacc-num {
  width: 24px; height: 24px; flex-shrink: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font: 800 13px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  background: rgba(255, 255, 255, 0.10); color: rgba(255, 255, 255, 0.7);
}
/* 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, #00A3DD); color: var(--navy, #132554); }
.lacc-title { flex: 1; font: 700 17px/1.2 var(--font-ui, 'Archivo', system-ui, sans-serif); color: #fff; }
.lacc-toggle {
  font-size: 24px; font-weight: 400; color: var(--signal, #00A3DD);
  line-height: 1; width: 20px; text-align: center; flex-shrink: 0;
}
.lacc-head:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: 2px; border-radius: 8px; }

/* The open panel: picture EMBEDDED above its text (this is the defining difference). */
.lacc-body {
  padding: 0 4px 16px 36px;
  animation: mvModalIn 260ms cubic-bezier(.2, .7, .3, 1);   /* keyframes in player.css */
}
.lacc-body-img {
  display: block; width: 100%; max-width: 560px;
  aspect-ratio: 8 / 3; object-fit: cover;
  border-radius: 12px; margin: 2px 0 12px;
  background: #E7EBF1; border: 1px solid #D5DCE8;
  box-shadow: inset 0 2px 22px rgba(19, 37, 84, 0.14);
  animation: mvImageFade 400ms ease-in-out;
}
.lacc-body-text {
  font: 400 17px/1.55 var(--font-ui, 'Archivo', system-ui, sans-serif);
  color: rgba(255, 255, 255, 0.82);
}

/* 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) {
  /* 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: #26407a; }
  .lacc {
    flex-direction: column; height: auto;
    min-height: calc(100dvh - var(--rc-header-h, 56px));
    box-sizing: border-box;
    padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));   /* clears the fixed bar */
    background: #26407a;               /* 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: 20px clamp(14px, 4vw, 28px) 26px; overflow-x: hidden; }
  .lacc-heading { font-size: 24px; }
  .lacc-list { gap: 10px; }

  /* THE CARD: the whole collapsible item, trigger + panel inside one surface. */
  .lacc-item {
    width: 100%; min-width: 0;
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 16px;
    background: rgba(11, 22, 49, 0.55);   /* 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: 12px;
    padding: 14px 16px; text-align: left;
  }
  .lacc-title { font-size: 16px; min-width: 0; overflow-wrap: anywhere; }
  .lacc-body { display: block; padding: 0 16px 18px; }

  /* 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: 12px; align-items: stretch;
  }
  .lacc-body-stage, .lacc-body-img { width: 100%; min-width: 0; border-radius: 12px; margin: 0; }

  .lacc-body-stage {
    display: flex; align-items: flex-end; justify-content: center;
    position: relative; overflow: hidden;
    height: auto; min-height: 0;
    border: 1px solid rgba(255, 255, 255, 0.10);
    background: linear-gradient(to bottom, rgba(53, 92, 164, 0.28) 0%, #26407a 55%, #16264f 100%);
    animation: mvImageFade 400ms 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: 0;
    background: linear-gradient(to bottom, #0b1631 0%, #16264f 55%, #1d3163 100%);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
  }
  .lacc-body-stage .lacc-character {
    position: relative; z-index: 1;
    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: 16px; margin: 14px 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) {
  .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: 3px 12px 4px 0;
    border-radius: 12px; overflow: hidden; position: relative;
    border: 1px solid rgba(255, 255, 255, 0.14);
    /* the same little room: light-navy wall with the floor shadow at the base */
    background: linear-gradient(to bottom, rgba(53, 92, 164, 0.28) 0%, #26407a 58%, #0f2653 100%);
  }
  .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: 1180px;
  padding: 18px clamp(16px, 3vw, 40px) 26px;
  display: flex; flex-direction: column;
}
.fc-eyebrow {
  font: var(--fw-extrabold, 800) 15px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  letter-spacing: 1.5px; text-transform: uppercase; color: var(--signal, #00A3DD);
}
.fc-heading {
  font-family: var(--font-display, 'Archivo', sans-serif);
  font-weight: 800; font-size: 30px; line-height: 1.15;
  color: #fff; margin: 5px 0 2px;
}
.fc-hint { margin: 0 0 14px; font: 500 15px/1.4 var(--font-ui, 'Archivo', sans-serif); color: rgba(255,255,255,0.65); }
.fc-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }

.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: 3px solid var(--signal, #00A3DD); outline-offset: 3px; border-radius: 14px; }
.fc-inner {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform 520ms cubic-bezier(.3, .7, .3, 1);
}
.fc-card.is-flipped .fc-inner { transform: rotateY(180deg); }
.fc-face {
  position: absolute; inset: 0; border-radius: 14px; overflow: hidden;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
/* resting face — light navy, signal band, dark navy title */
.fc-front {
  background: #26407a;
  border: 1px solid rgba(255, 255, 255, 0.14);
  display: flex; align-items: center; justify-content: center;
}
.fc-band {
  width: 100%; padding: 14px 18px; text-align: center;
  background: var(--signal, #00A3DD); color: var(--navy, #132554);   /* dark navy title on signal */
  font: 800 clamp(17px, 1.6vw, 21px)/1.2 var(--font-ui, 'Archivo', sans-serif);
  letter-spacing: 0.3px;
}
.fc-flip-hint {
  position: absolute; right: 12px; bottom: 8px;
  font: 400 26px/1 var(--font-ui, 'Archivo', sans-serif); color: rgba(255, 255, 255, 0.55);
}
.fc-card.is-seen .fc-flip-hint { color: var(--correct, #12A66A); }
/* flipped face — white interior, LIGHT-NAVY boundary all the way around */
.fc-back {
  transform: rotateY(180deg);
  background: #fff; border: 3px solid #26407a;
  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: #EEF1F6; position: relative; }
.fc-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.fc-img--contain { object-fit: contain; padding: 6px; box-sizing: border-box; }
.fc-body {
  flex: 1 1 38%; min-height: 0; overflow-y: auto;
  padding: 10px 14px 12px;
  font: 400 14px/1.45 var(--font-ui, 'Archivo', sans-serif); color: var(--ink, #16203A);
}
/* 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) {
  .fc-body { font-size: 17px; line-height: 1.5; padding: 12px 18px 14px; }
  .fc-hint { font-size: 16px; }
}
@media (max-width: 1024px) { .fc-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px)  { .fc-grid { grid-template-columns: 1fr; } .fc-heading { font-size: 24px; } }
@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: 1180px;
  padding: 18px clamp(16px, 3vw, 40px) 26px;
  display: flex; flex-direction: column;
}
.tab-eyebrow {
  font: var(--fw-extrabold, 800) 15px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  letter-spacing: 1.5px; text-transform: uppercase; color: var(--signal, #00A3DD);
}
.tab-heading {
  font-family: var(--font-display, 'Archivo', sans-serif);
  font-weight: 800; font-size: 30px; line-height: 1.15;
  color: #fff; margin: 5px 0 16px;
}
.tab-instruction {
  font: 400 17px/1.5 var(--font-ui, 'Archivo', sans-serif);
  color: rgba(255, 255, 255, 0.8);
  margin: -8px 0 16px;
}
.tab-folder { display: flex; flex-direction: column; min-height: 0; }
.tab-strip { display: flex; gap: 6px; padding: 0 14px; }
.tab-tab {
  border: 1px solid rgba(255, 255, 255, 0.16); border-bottom: none;
  border-radius: 12px 12px 0 0;
  padding: 12px 22px 10px; cursor: pointer;
  background: #1d3163;                                  /* resting tab */
  color: rgba(255, 255, 255, 0.85);
  font: 700 15px/1 var(--font-ui, 'Archivo', sans-serif);
  position: relative; top: 1px;                          /* overlaps the body border for the merge */
}
.tab-tab.is-seen::after { content: " ✓"; color: var(--correct, #12A66A); font-weight: 800; }
.tab-tab.is-active {
  background: #26407a; color: #fff;                      /* same surface as the body = folder merge */
  border-color: rgba(255, 255, 255, 0.22);
  z-index: 1;
}
.tab-tab:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: -3px; }
.tab-panel {
  background: #26407a; border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 14px; padding: 22px 26px;
  display: flex; gap: 22px; align-items: flex-start;
  animation: mvModalIn 240ms cubic-bezier(.2, .7, .3, 1);   /* the accordion's layer-in */
}
.tab-panel-img {
  flex: 0 0 40%; min-width: 0; max-width: 480px;
  aspect-ratio: 4 / 3; object-fit: cover;   /* squarer frame (author 2026-07-24), was 8:3 */
  border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.14);
  background: #E7EBF1;
}
.tab-panel-text { flex: 1; min-width: 0; }
.tab-panel-title { margin: 0 0 8px; font: 800 20px/1.2 var(--font-ui, 'Archivo', sans-serif); color: #fff; }
.tab-panel-body { font: 400 16px/1.55 var(--font-ui, 'Archivo', sans-serif); color: rgba(255, 255, 255, 0.85); }

/* tablet/phone: VERTICAL tabs on the left-hand side */
@media (max-width: 1024px) {
  .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: 6px; padding: 14px 0; flex: 0 0 auto; max-width: none; }
  .tab-tab {
    border: 1px solid rgba(255, 255, 255, 0.16); border-right: none;
    border-radius: 12px 0 0 12px;
    top: 0; left: 1px;                                   /* overlap the body's left border */
    padding: 16px 10px; text-align: left;
    writing-mode: vertical-rl; text-orientation: mixed;   /* text runs down the tab */
    white-space: nowrap;
  }
  .tab-tab.is-active { left: 1px; }
  .tab-panel { flex-direction: column; gap: 14px; border-radius: 0 14px 14px 14px; min-width: 0; flex: 1; }
  .tab-panel-img { flex: 0 0 auto; width: 100%; max-width: 100%; }
  .tab-heading { font-size: 24px; }
}
@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: 30;
  background: rgba(8, 15, 34, 0.72);
  display: flex; align-items: center; justify-content: center;
  animation: mvModalIn 240ms cubic-bezier(.2, .7, .3, 1);
}
.tab-conv-modal {
  position: relative;
  width: min(1120px, 92%); height: min(660px, 90%);
  background: #1d3163;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 20px;
  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: rgba(255, 255, 255, 0.35);
}
/* 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: 1920 / 821;
  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: 2; container-type: size; }
.tab-conv-shell > img { width: 100%; height: 100%; display: block; }
.tab-conv-shelltext {
  position: absolute; padding: 10px; overflow: hidden;
  display: flex; align-items: center; justify-content: center; text-align: center;
  font-family: var(--font-ui, 'Archivo', system-ui, sans-serif);
  font-weight: 600; line-height: 1.25; color: #16203A;
}
.tab-conv-shelltext p { margin: 0; }
.tab-conv-char {
  position: absolute; bottom: 0;
  height: 72%; width: auto;
  filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.35));
}
.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: #fff; color: #16203A;
  border: 3px solid #0E1C42; border-radius: 15px;
  padding: 14px 18px;
  box-shadow: 0 6px 16px rgba(8, 12, 26, 0.35);
  animation: mvModalIn 200ms cubic-bezier(.2, .7, .3, 1);
}
.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: #0E1C42;
}
.tab-conv-bubble::before {
  content: ""; position: absolute; top: calc(100% - 5px); width: 0; height: 0;
  border: 10px solid transparent; border-top-color: #fff; z-index: 1;
}
.tab-conv-bubble.is-left::after   { left: 44px; }
.tab-conv-bubble.is-left::before  { left: 48px; }
.tab-conv-bubble.is-right::after  { right: 44px; }
.tab-conv-bubble.is-right::before { right: 48px; }
.tab-conv-speaker {
  display: block;
  font: var(--fw-extrabold, 800) 13px/1 var(--font-ui, 'Archivo', sans-serif);
  letter-spacing: 1.2px; text-transform: uppercase;
  color: var(--signal, #00A3DD);
  margin-bottom: 8px;
}
.tab-conv-text { margin: 0; font: 400 19px/1.5 var(--font-ui, 'Archivo', sans-serif); }
.tab-conv-btn {
  position: absolute; bottom: 26px; left: 50%; transform: translateX(-50%);
  border: none; border-radius: var(--radius-pill, 100px); padding: 15px 40px; cursor: pointer;
  font: 800 16px/1 var(--font-ui, 'Archivo', sans-serif); letter-spacing: 0.3px;
  background: var(--signal, #00A3DD); color: var(--navy, #132554);
}
.tab-conv-btn:hover { background: var(--signal-hover, #008EC1); }
.tab-conv-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(0, 163, 221, 0.55); }
@media (max-width: 1024px) {
  /* 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; }
  .tab-conv-modal { width: 94%; height: min(560px, 86vh); }
  /* 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: 18px; background: #0E1C42; color: #fff; }
  .tab-conv-btn:hover { background: var(--navy, #132554); }
}
/* 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 84px;
  font-family: var(--font-ui, 'Archivo', system-ui, sans-serif);
}
/* 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 750ms ease 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: 400; }

/* 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: 10px; right: 12px; z-index: 5;
  width: 40px; height: 40px; border: 0; border-radius: 50%;
  background: rgba(14, 28, 66, 0.55); color: #fff; cursor: pointer;
  font: 400 26px/1 system-ui, sans-serif;
}
.tab-conv-close:hover { background: rgba(14, 28, 66, 0.8); }
.tab-conv-close:focus-visible { outline: 3px solid var(--signal, #00A3DD); 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: 2px solid var(--signal, #00A3DD);
  animation: convSayPulse 1.8s 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 1.8s 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 rgba(0, 163, 221, 0.20); }
  50%      { box-shadow: 0 0 0 7px rgba(0, 163, 221, 0.45); }
}
@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 rgba(0, 163, 221, 0.35); }
  button.tab-conv-shell.tab-conv-say img { animation: none; filter: drop-shadow(0 0 5px rgba(0, 163, 221, 0.7)); }
}

/* 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 300ms ease; }
@media (prefers-reduced-motion: reduce) { .tab-conv-shell { transition: none; } }
.tab-conv-say:hover { filter: brightness(1.05); }
.tab-conv-say:focus-visible { outline: 3px solid var(--signal, #00A3DD); 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, #007BA6); 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, #007BA6);
  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: 22px; padding-top: 22px;
  border-top: 1.5px solid rgba(255, 255, 255, 0.16);
}
.sc-review--practice .sc-review-title { margin-bottom: 6px; }
.sc-review-note {
  font: 500 14px/1.5 var(--font-ui, system-ui, sans-serif);
  color: rgba(255, 255, 255, 0.66); margin-bottom: 16px;
}
/* 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 4px; max-width: 520px; }
.sc-awaiting .sc-review--practice { border-top: none; margin-top: 8px; }

/* 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: 8px; margin-top: 4px;
  font: 600 13px/1.3 var(--font-ui, system-ui, sans-serif);
  color: var(--muted, #8A93A3);
}
.sc-q-remediated {
  padding: 2px 8px; border-radius: var(--radius-pill, 100px);
  background: rgba(0, 163, 221, 0.12); color: var(--signal, #00A3DD);
  font: var(--fw-extrabold, 800) 11px/1.6 var(--font-ui, system-ui, sans-serif);
  letter-spacing: 0.4px; 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: 18px 16px 24px; }
.swc { width: 100%; max-width: 720px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
/* 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: 800 26px/1.2 var(--font-head, 'Archivo', system-ui, sans-serif); color: #fff;
}
.swc-instr {
  margin: 0; text-align: center; max-width: 620px;
  font: 400 16px/1.45 var(--font-ui, 'Archivo', system-ui, sans-serif);
  color: rgba(255, 255, 255, 0.92);
}
.swc-progress {
  margin: 2px 0 0;
  font: 800 12px/1 var(--font-head, 'Archivo', sans-serif); letter-spacing: 1px;
  text-transform: uppercase; color: var(--signal, #00A3DD);
}

/* the card + its two directional cues, side by side on wide screens */
.swc-stage { display: flex; align-items: center; justify-content: center; gap: 14px; width: 100%; }
.swc-cue {
  flex: 0 0 auto; opacity: 0.25; transition: opacity 160ms ease;
  font: 800 12px/1 var(--font-head, 'Archivo', sans-serif); letter-spacing: 1px;
  text-transform: uppercase; color: var(--navy, #132554);
  writing-mode: vertical-rl;
}
.swc-cue--left { transform: rotate(180deg); }
.swc-cue.is-on { opacity: 1; color: var(--signal, #00A3DD); }

.swc-card {
  flex: 1 1 auto; max-width: 560px; box-sizing: border-box;
  background: #fff; border: 1px solid var(--line, #DCE2EC); border-radius: 16px;
  padding: 14px 14px 12px; box-shadow: 0 10px 26px rgba(19, 37, 84, 0.14);
  /* 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 220ms cubic-bezier(.2,.7,.3,1);
}
.swc-card.is-dragging { cursor: grabbing; }
.swc-card:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: 3px; }

.swc-media {
  position: relative; width: 100%; aspect-ratio: 4 / 3;   /* inline override wins if authored */
  overflow: hidden; border-radius: 10px; background: #EEF1F6;
  border: 1px solid var(--line, #DCE2EC);
}
.swc-img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.swc-prompt {
  margin: 12px 2px 0;
  font: 700 17px/1.4 var(--font-ui, 'Archivo', system-ui, sans-serif); color: var(--ink, #16203A);
}
.swc-context { margin: 6px 2px 0; font: 400 15px/1.45 var(--font-ui, 'Archivo', sans-serif); color: #5A6577; }
/* 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: 4px 0 0; padding: 7px 18px; max-width: 620px; text-align: center;
  border-radius: var(--radius-pill, 100px);
  background: rgba(0, 163, 221, 0.16);
  border: 1px solid rgba(0, 163, 221, 0.45);
  font: 800 12px/1.35 var(--font-head, 'Archivo', sans-serif); letter-spacing: .6px;
  text-transform: uppercase; color: #fff;
}

/* Both controls, always, on every platform. 44px minimum touch target per WCAG 2.5.5. */
.swc-controls { display: flex; gap: 12px; justify-content: center; width: 100%; max-width: 560px; margin-top: 12px; }
.swc-btn {
  flex: 1 1 0; min-height: 48px; min-width: 44px; padding: 12px 18px;
  border-radius: var(--radius-pill, 100px); border: 2px solid var(--navy, #132554);
  background: #fff; color: var(--navy, #132554); cursor: pointer;
  font: 800 16px/1.1 var(--font-ui, 'Archivo', sans-serif);
}
.swc-btn--right { background: var(--navy, #132554); color: #fff; }
.swc-btn:hover:not(:disabled) { border-color: var(--signal, #00A3DD); }
.swc-btn:focus-visible { outline: 3px solid var(--signal, #00A3DD); outline-offset: 2px; }
.swc-btn:disabled { opacity: .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 520ms cubic-bezier(.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: 12px; border-radius: 12px; padding: 18px 18px 72px; 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: 18px; 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: rgba(18, 166, 106, 0.10); box-shadow: inset 0 0 0 2px var(--correct, #12A66A); }
.swc-face--back.is-wrong { background: rgba(229, 70, 62, 0.09); box-shadow: inset 0 0 0 2px var(--incorrect, #E5463E); }
/* 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: 3px solid var(--signal, #00A3DD); outline-offset: 2px; border-radius: 8px; }
@media (prefers-reduced-motion: reduce) { .swc-card-inner { transition: none; } }

.swc-fb-title { font: 800 15px/1.2 var(--font-head, 'Archivo', sans-serif); color: var(--navy, #132554); }
.swc-fb-body { margin: 6px 0 0; font: 400 15px/1.5 var(--font-ui, 'Archivo', sans-serif); color: var(--ink, #16203A); }
.swc-fb-btn {
  min-height: 44px; padding: 10px 22px; border: none; cursor: pointer;
  border-radius: var(--radius-pill, 100px);
  background: var(--signal, #00A3DD); color: var(--navy, #132554);
  font: 800 15px/1 var(--font-ui, 'Archivo', sans-serif);
}
.swc-fb-btn:focus-visible { outline: 3px solid var(--navy, #132554); 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: 32px 24px;
  width: 100%; max-width: 560px; margin: 0 auto; box-sizing: border-box;
  background: #fff; border: 1px solid var(--line, #DCE2EC); border-radius: 16px;
  box-shadow: 0 10px 26px rgba(19, 37, 84, 0.14);
}
.swc-done-mark {
  width: 56px; height: 56px; margin: 0 auto 12px; border-radius: 50%;
  background: var(--correct, #12A66A); color: #fff;
  font: 800 30px/56px var(--font-head, 'Archivo', sans-serif);
}
.swc-done-title { font: 800 20px/1.2 var(--font-head, 'Archivo', sans-serif); color: var(--navy, #132554); }
.swc-done-body { margin: 8px 0 0; font: 400 16px/1.5 var(--font-ui, 'Archivo', sans-serif); color: var(--ink, #16203A); }

@media (max-width: 1024px) {
  /* 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: 560px; }
}
@media (max-width: 600px) {
  .tpl-page--swipe { padding: 14px 14px 20px; }
  .swc-title { font-size: 22px; }
  /* Body text does NOT shrink below the course's normal size on phone. */
  .swc-instr { font-size: 16px; }
  .swc-prompt { font-size: 17px; }
  .swc-context { font-size: 15px; }
  /* 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: 12px 10px; font-size: 15px; }   /* 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: 6px; left: 8px; z-index: 5;
  width: 22px; height: 22px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 4px;
  background: rgba(0, 0, 0, 0.28); color: #fff;
  font: 600 15px/1 var(--font-ui, 'Archivo', system-ui, sans-serif);
  cursor: pointer; opacity: 0.55;
}
.mv-dev-modal-close:hover { opacity: 1; }
.mv-dev-modal-close:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(0, 163, 221, 0.6); }

/* [REVIEW OVERLAY] Review-notice modal (Cloudflare *.pages.dev only). Applied by
   authoring/scripts/build_review_site.py — NOT part of deliverable/. */
.dev-notice-scrim {
  position: fixed; inset: 0; z-index: 9000;
  display: flex; align-items: center; justify-content: center; padding: 24px;
  background: rgba(19, 37, 84, 0.55);
}
.dev-notice {
  background: var(--white); color: var(--ink);
  width: 100%; max-width: 520px;
  max-height: calc(100vh - 48px);
  max-height: calc(100dvh - 48px);
  overflow-y: auto;
  border-radius: var(--radius-card); border-top: 4px solid var(--signal);
  box-shadow: var(--shadow-frame);
  padding: 28px 28px 24px;
}
.dev-notice-eyebrow {
  font: 700 12px/1 var(--font-sans); letter-spacing: .08em; text-transform: uppercase;
  color: var(--signal); margin-bottom: 10px;
}
.dev-notice-title { font: 800 24px/1.15 var(--font-sans); color: var(--navy); margin: 0 0 12px; }
.dev-notice-body { font: 400 15px/1.5 var(--font-sans); color: var(--ink); margin: 0 0 12px; }
.dev-notice-list { margin: 4px 0 22px; padding-left: 20px; }
.dev-notice-list li { font: 400 15px/1.5 var(--font-sans); color: var(--ink); margin-bottom: 8px; }
.dev-notice-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.dev-notice-btn {
  flex: 1 1 auto; min-width: 190px; cursor: pointer;
  font: 700 15px/1 var(--font-sans); padding: 13px 18px;
  border-radius: var(--radius-button); border: var(--border-control) solid var(--border);
  background: var(--white); color: var(--navy);
}
.dev-notice-btn:hover { border-color: var(--navy); }
.dev-notice-btn.is-primary {
  background: var(--signal); border-color: var(--signal); color: var(--white);
}
.dev-notice-btn.is-primary:hover { background: var(--signal-hover); border-color: var(--signal-hover); }
.dev-notice-note {
  font: 400 13px/1.5 var(--font-sans); color: var(--muted);
  margin: 16px 0 0; text-align: center;
}
