/* ============================================================================
   Screen-type layout styles. One block per renderer.
   Currently: pptSlide (slideStage) — a PowerPoint-rendered image as the stage,
   with React-driven caption, character, hotspot, choice, and feedback overlays.
   Visual composition lives in PowerPoint; this file only positions the
   interactive/accessible overlays on top of it. Do not restyle the artwork.
   ============================================================================ */

/* --- stage: fills the CoursePlayer content zone --- */
.ppt-stage { position: absolute; inset: 0; overflow: hidden; background: var(--surface-stage, #1A2E60); }
.ppt-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; display: block; background: #0A1023; }

/* Responsive mode (tablet/phone): a NARRATIVE slide keeps its fixed 1920x821 composition
   (CLAUDE.md — only template layouts reflow). The ResponsiveShell content area is portrait,
   so without this the absolutely-inset stage stretches to the viewport and the WHOLE scene —
   background, characters (which fill their wrapper 100%), and bubble positions — distorts
   vertically. Aspect-lock the stage to a proportional band instead: everything then scales
   together, and each pose canvas fills its wrapper at the correct aspect (no distortion). */
html.responsive .ppt-stage, body.responsive .ppt-stage {
  position: relative; inset: auto; width: 100%; height: auto; aspect-ratio: 1920 / 821;
}

/* ============================================================================
   Comic two-shot layout — responsive (tablet/phone) NARRATIVE scenes only. Comic panel with both
   characters' heads on top (speaker grows to ~2/3, their line types on in step with the clip),
   the live scene below, inside the ResponsiveShell chrome. Desktop renders the full stage instead.
   ============================================================================ */
.ppt-comic-layout {
  display: flex; flex-direction: column; min-height: 0;
  height: calc(100dvh - var(--rc-header-h, 56px) - 96px);   /* fill between the header and bottom bar */
}
/* top: signal-blue comic panel. The bottom scene gets the larger share of the height —
   the heads + bubble read fine smaller, and the live scene is what the learner watches. */
.ppt-comic {
  flex: 1 1 45%; min-height: 0; position: relative; overflow: hidden;
  display: flex; align-items: stretch; gap: 2vw; padding: 1.4vh 3vw 2.2vh;
  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;
}
/* Each side is a grid: the bubble occupies the flexible top row, head + name pin to the bottom.
   The top row is minmax(0, 1fr) — capped at its fair share, NOT allowed to grow with the bubble —
   so a long (multi-line) speaker line can never push that head lower than the silent listener's.
   Both heads stay bottom-aligned regardless of line length. */
.ppt-comic-side {
  /* The VISIBLE head frame size is part of the shared speaker state — not left to the flex side's
     width, which the old `min(90%, 190px)` head cap defeated (on tablet both sides exceed 211px, so
     both heads stayed 190px even as the speaker's side grew: the layout grew, the frame didn't).
     Listener size by default; the speaker override below enlarges it, transitioned in lock-step with
     the side's flex-grow so intro-01, Observe, and intro-02 share ONE size+motion model. */
  --comic-head-size: clamp(105px, 23vw, 150px);
  flex: 1 1 0; min-width: 0;
  display: grid; grid-template-rows: minmax(0, 1fr) auto auto; justify-items: center; row-gap: 6px;
  transition: flex-grow .4s cubic-bezier(.3,.7,.3,1);
}
.ppt-comic-side.is-speaking { --comic-head-size: clamp(150px, 32vw, 210px); flex-grow: 2; }  /* speaker ~2/3 + bigger head */
/* square comic panel: bold black outer line + crisp white inner keyline. Width-sized (capped) so
   the speaker's wider panel gives a bigger head; height follows via aspect-ratio. */
/* head + its floating bubble; pinned to the bottom of the side by the grid's 1fr top row */
.ppt-comic-headwrap {
  grid-row: 2; position: relative; width: 100%;
  display: flex; justify-content: center; align-items: flex-end;
}
.ppt-comic-head {
  /* width is the plain custom-property length (NOT min(100%, …)): Chrome won't interpolate a
     `width` transition whose value is a min()/clamp() mixing a percentage with a var, so the head
     froze at the listener size. A bare var() animates cleanly; max-width:100% keeps the overflow
     guard without entering the transition path. */
  width: var(--comic-head-size); max-width: 100%; aspect-ratio: 1; border-radius: 8px; overflow: hidden; position: relative;
  background: radial-gradient(circle at 50% 40%, #163264 0%, #0a1633 100%);
  border: 5px solid #000; box-shadow: 0 8px 18px rgba(8,12,26,.4);
  transition: width .4s cubic-bezier(.3,.7,.3,1), filter .35s ease, opacity .35s ease;
}
/* white inner keyline drawn on a layer BENEATH the portrait, so the shoulders paint OVER its
   bottom edge (the intended "superimposed in front" look). It used to live in the head's own
   `box-shadow: inset` — but a positioned/transformed portrait paints above that shadow while an
   un-transformed one does not, so a flipped head (Sue) covered the keyline and a non-flipped one
   (Loretta) had the keyline render on top of her shoulders. Splitting it out + z-index makes the
   layering identical for every head, flipped or not. */
.ppt-comic-head::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; z-index: 0; pointer-events: none;
  box-shadow: inset 0 0 0 3px #fff;
}
.ppt-comic-head img { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; object-fit: contain; object-position: center bottom; display: block; }
/* flip on the IMG, not the head: the frame is symmetric so it looks identical, but it frees the head's
   own transform for the branch-expand translateX (Loretta's right-edge lock) without clobbering her flip. */
.ppt-comic-head.flip img { transform: scaleX(-1); }
.ppt-comic-side:not(.is-speaking) .ppt-comic-head { filter: saturate(.7) brightness(.82); opacity: .9; }
.ppt-comic-name {
  grid-row: 3; margin-top: 6px; font-weight: 800; letter-spacing: .5px; color: #fff; text-transform: uppercase;
  font-size: clamp(10px, 2.4vw, 14px); text-shadow: 0 1px 0 #0E1C42, 0 2px 4px rgba(8,12,26,.4);
}
/* speaker's bubble: fills the flexible top row, pinned to its bottom so its tail sits just above the head */
.ppt-comic-bubble {
  /* floats above the head (out of flow) so it can't affect head alignment; centered over the head,
     ~2/3-panel wide via viewport units so a long line wraps to few lines instead of pushing down */
  position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%);
  width: min(62vw, 440px);
  background: #fff; color: #16203A; border: 3px solid #0E1C42; border-radius: 15px; padding: 8px 12px;
  font-weight: 650; line-height: 1.2; font-size: clamp(12px, 2.9vw, 16px);
  box-shadow: 0 6px 16px rgba(8,12,26,.35); text-align: left;
}
.ppt-comic-bubble::after  { content: ""; position: absolute; top: 100%; left: 34px; width: 0; height: 0; border: 11px solid transparent; border-top-color: #0E1C42; }
.ppt-comic-bubble::before { content: ""; position: absolute; top: calc(100% - 4px); left: 37px; width: 0; height: 0; border: 8px solid transparent; border-top-color: #fff; z-index: 1; }
.ppt-comic-side:last-child .ppt-comic-bubble::after  { left: auto; right: 34px; }
.ppt-comic-side:last-child .ppt-comic-bubble::before { left: auto; right: 37px; }
.ppt-comic-caret { display: inline-block; width: 2px; height: 1.05em; margin-left: 1px; background: #00A3DD; vertical-align: text-bottom; animation: comicBlink 1s steps(1) infinite; }
@keyframes comicBlink { 50% { opacity: 0; } }
/* Conversation Decision comic panel: heads row on top, then the current line, then (ask phase) the
   choices as a vertical tappable stack. Column layout + scroll so a long stack never overflows. */
.cvo-comic { flex-direction: column; align-items: stretch; gap: 1.2vh; overflow-y: auto; padding: 1.4vh 4vw 2vh; }
.cvo-comic-heads { flex: 0 0 auto; display: flex; justify-content: center; gap: 6vw; }
.cvo-comic-heads .ppt-comic-side { flex: 0 1 auto; grid-template-rows: auto auto; }
.cvo-comic-heads .ppt-comic-head { width: min(26vw, 112px); }
.cvo-comic-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: 1.1vh; }
.cvo-comic-say {
  background: #fff; color: #16203A; border: 3px solid #0E1C42; border-radius: 14px; padding: 9px 13px;
  font-weight: 600; line-height: 1.32; font-size: clamp(13px, 3.5vw, 16px); box-shadow: 0 5px 14px rgba(8,12,26,.3);
}
.cvo-comic-who { color: var(--signal, #00A3DD); text-transform: uppercase; letter-spacing: .4px; }
.cvo-comic-options { display: flex; flex-direction: column; gap: 1vh; margin-top: .4vh; }
.cvo-comic-option {
  min-height: 44px; text-align: left; background: #fff; color: #16203A;
  border: 3px solid #0E1C42; border-radius: 14px; padding: 10px 14px;
  font-weight: 650; line-height: 1.25; font-size: clamp(13px, 3.5vw, 16px);
  cursor: pointer; box-shadow: 0 3px 0 rgba(14,28,66,.55); transition: box-shadow .15s ease, border-color .15s ease;
}
/* PULSES LIKE ITS DESKTOP COUNTERPART (author 2026-07-31: every clickable speech bubble in the
   course signals the same way). On desktop the Conversation Decision's choice bubbles already
   pulsed (.cvo-choice, cvoPulse); these are the same choice on a phone, and they sat static —
   the one inconsistency in the affordance. Its own keyframe because these are comic-styled
   boxes, not transparent bubble art: the pulse rides ON TOP of the 3px comic drop-shadow rather
   than replacing it, which a shared keyframe would have done. */
.cvo-comic-option { animation: cvoComicPulse 1.8s ease-in-out infinite; }
@keyframes cvoComicPulse {
  0%, 100% { box-shadow: 0 3px 0 rgba(14,28,66,.55), 0 0 0 2px rgba(0,163,221,0.20); }
  50%      { box-shadow: 0 3px 0 rgba(14,28,66,.55), 0 0 0 7px rgba(0,163,221,0.45); }
}
/* A running animation outranks a static declaration, so hover/focus must STOP it or their
   own ring never appears. Same reason the reduced-motion rule below turns it off. */
.cvo-comic-option:hover, .cvo-comic-option:focus-visible {
  animation: none;
  border-color: var(--signal, #00A3DD); box-shadow: 0 0 0 3px rgba(0,163,221,.45); outline: none;
}
@media (prefers-reduced-motion: reduce) {
  .cvo-comic-option { animation: none; box-shadow: 0 3px 0 rgba(14,28,66,.55), 0 0 0 3px rgba(0,163,221,0.35); }
}
.cvo-comic-option:active { transform: translateY(1px); box-shadow: 0 1px 0 rgba(14,28,66,.55); }

/* Conversation Decision on the intro-01-style comic panel: the full-size heads stay the same
   (ppt-comic-heads keeps the intro-01 row), and the choices stack BELOW them while the clock is
   held at the decision. Column + scroll so a long stack never overflows the panel. */
.ppt-comic-decide { flex-direction: column; gap: 1.1vh; overflow-y: auto; }
.ppt-comic-decide .ppt-comic-heads { display: flex; align-items: stretch; gap: 2vw; flex: 1 1 auto; min-height: 0; }
/* narrative comic with a re-readable HISTORY band on top (intro-01/Observe): the panel becomes a column
   — a slim scroll-up-able strip of past lines (chat bubbles, SAME styling as the conversation thread),
   then the heads row. The heads are width-sized, so they don't shrink; the strip just uses the empty
   space above them, and older lines scroll up out of view but stay reachable. */
.ppt-comic-log { display: flex; flex-direction: column; padding: 0; }   /* the scroller owns the panel, flush under the header */
/* the whole upper half is one bottom-anchored SCROLLER: finished lines stack above, the heads + current
   line are the floor at the bottom. overflow-y auto + a top auto-margin pin the floor to the bottom and
   let older lines slide up behind the chrome (scroll up to re-read). */
.ppt-comic-scroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column;
}
.ppt-comic-scroll > :first-child { margin-top: auto; }
.ppt-comic-history-log { display: flex; flex-direction: column; gap: .8vh; padding: 1vh 4vw .4vh; }
/* the heads floor holds ALL of the panel except a slim peek slot at the very top — so at rest the newest
   finished line is scrolled up to just its bottom edge (behind the chrome, its LOWEST allowed spot) and
   the current speaker's bubble stays clear below it. Scroll up to pull the full history down into view. */
.ppt-comic-log .ppt-comic-heads { display: flex; align-items: stretch; gap: 2vw; flex: 0 0 auto; min-height: calc(100% - clamp(24px, 3.4vh, 32px)); box-sizing: border-box; padding-bottom: 1.1vh; }   /* name rests ~midway on the dotted line, not jammed to the boundary */
.cvo-decision { display: flex; flex-direction: column; gap: 1vh; flex: 0 0 auto; }
.cvo-decision-head { font: 800 clamp(11px, 2.8vw, 14px)/1.2 var(--font-head, "Archivo", sans-serif);
  letter-spacing: .5px; text-transform: uppercase; color: #fff; text-align: center; margin-bottom: .3vh;
  text-shadow: 0 1px 0 #0E1C42; }
/* desktop (non-comic): the choices float over the stage, centered near the bottom */
.ppt-stage > .cvo-decision { position: absolute; left: 50%; bottom: 6%; transform: translateX(-50%);
  width: min(78%, 900px); z-index: 6; }

/* ============================================================================
   Conversation Decision as a CHAT-FLOW (tablet/phone) — the Question of Consequences.
   A messaging thread: each dialogue line is a name-labelled chat bubble on its speaker's
   side (Sue LEFT, tinted navy = "your reply"; Loretta RIGHT, white), the newest typing on
   just above a PERSISTENT head row pinned to the BOTTOM (intro-01's shared shift+grow — NO
   rise). The scroll owns overflow, so the active line can never clip; older turns scroll up
   and stay re-readable. The decision = Sue's reply chips at the bottom of the chat, above the
   heads. Replaces the old floor-pane / rise / peek / history-lift machinery (2026-07-20
   redesign; that CSS is archived in documentation/archive/comic-thread-rise-choreography/).
   ============================================================================ */
/* the chat panel takes the larger share (60%); the scene keeps 40% so its figures stay fully visible.
   Scoped to .cvo-thread so intro-01/Observe (ComicPanel) keep their 45/55 split. */
.ppt-comic.cvo-thread { flex: 1 1 60%; flex-direction: column; align-items: stretch; gap: 0; padding: 0; }
.cvo-thread + .ppt-comic-scene { flex: 1 1 40%; }

/* the scrolling thread — bottom-anchored; the active line + chips sit just above the heads */
.cvo-thread-scroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 1vh; padding: 1.4vh 4vw 1vh;
}
/* bottom-anchor while it fits; once it overflows the top stays reachable — scroll UP to re-read the
   whole conversation. (JS keeps it pinned to the bottom as the active line types, unless you scroll up.) */
.cvo-thread-scroll > :first-child { margin-top: auto; }
.cvo-thread-log { display: flex; flex-direction: column; gap: 1vh; }

/* each turn: a name label + a chat bubble, aligned to (and sized against) its speaker's side */
.cvo-th-line { display: flex; flex-direction: column; max-width: 82%; }
.cvo-th-line.is-left  { align-self: flex-start; align-items: flex-start; }
.cvo-th-line.is-right { align-self: flex-end;   align-items: flex-end; }
.cvo-th-name {
  margin: 0 8px 3px; font: 800 clamp(9px, 2.3vw, 12px)/1 var(--font-head, "Archivo", sans-serif);
  letter-spacing: .5px; text-transform: uppercase; color: #ff6b86;
  text-shadow: 0 1px 0 rgba(14,28,66,.5);
}
.cvo-th-bubble {
  background: #fff; color: #16203A; border: 3px solid #0E1C42; border-radius: 15px;
  padding: 8px 12px; font-weight: 650; line-height: 1.28; font-size: clamp(12px, 3vw, 15px);
  box-shadow: 0 4px 12px rgba(8,12,26,.3); animation: cvoThIn 300ms cubic-bezier(.2,.7,.3,1) both;
}
@keyframes cvoThIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* the reply chips — their OWN fixed band between the scrolling chat and the heads (a sibling of the
   scroll, NOT inside it), so the full choice set + the "Choose your response" callout are always visible
   and can never clip off the top. */
.cvo-thread-chips { flex: 0 0 auto; padding: .6vh 4vw .4vh; }
/* ENTRANCE: when the decision opens the callout fades in and the three options SLIDE UP from below,
   staggered (like the old gate reveal) — at the same moment the heads shrink (their shared width
   transition), so the two read as one move. fill-mode `backwards` (NOT forwards): the option holds its
   from-state during its stagger delay, then after settling reverts to its natural opacity — so the pick
   resolve below (chosen highlights / others dim) still takes effect instead of being pinned opaque. */
.cvo-thread-chips .cvo-decision-head { animation: cvoChipHead 320ms ease backwards; }
.cvo-thread-chips .cvo-comic-option {
  animation: cvoChipIn 460ms cubic-bezier(.3,.7,.3,1) backwards,
             cvoComicPulse 1.8s ease-in-out infinite;   /* entrance THEN pulse, composed */
}
.cvo-thread-chips .cvo-comic-option:nth-child(2) { animation-delay: 90ms; }   /* child 1 = the callout */
.cvo-thread-chips .cvo-comic-option:nth-child(3) { animation-delay: 175ms; }
.cvo-thread-chips .cvo-comic-option:nth-child(4) { animation-delay: 260ms; }
@keyframes cvoChipHead { from { opacity: 0; } to { opacity: 1; } }
@keyframes cvoChipIn { from { opacity: 0; transform: translateY(48px); } to { opacity: 1; transform: none; } }
/* EXIT on pick — plays inside the ~1.75s pick→answer window, before the band unmounts (renderConvDecision
   tags .is-resolved + is-chosen/is-unchosen): the chosen chip briefly highlights then slides UP + fades,
   the unchosen fade + settle down, the callout fades. forwards fill is fine — the whole band is leaving. */
.cvo-decision.is-resolved .cvo-decision-head { animation: cvoChipFade .4s ease forwards; }
.cvo-thread-chips .cvo-comic-option.is-unchosen { pointer-events: none; animation: cvoChipOutUn .5s ease forwards; }
.cvo-thread-chips .cvo-comic-option.is-chosen {
  pointer-events: none; border-color: var(--signal, #00A3DD); box-shadow: 0 0 0 3px rgba(0,163,221,.5);
  animation: cvoChipOutChosen .8s cubic-bezier(.3,.7,.3,1) forwards;
}
@keyframes cvoChipFade { to { opacity: 0; } }
@keyframes cvoChipOutUn { to { opacity: 0; transform: translateY(10px); } }
@keyframes cvoChipOutChosen { 25% { transform: translateY(0); opacity: 1; } to { opacity: 0; transform: translateY(-46px); } }

/* the PERSISTENT head row pinned to the bottom — intro-01's shared .ppt-comic-side shift+grow (no rise).
   The current speaker's head grows + its column widens; the listener's shrinks/dims. */
.cvo-thread-heads {
  flex: 0 0 auto; display: flex; align-items: flex-end; gap: 2vw;
  padding: 0 3vw 1.2vh; box-sizing: border-box;
}
/* head + name only (no floating-bubble row): drop .ppt-comic-side's 1fr top row so the head sits low */
.cvo-thread-heads .ppt-comic-side { grid-template-rows: auto auto; }
.cvo-thread-heads .ppt-comic-headwrap { grid-row: 1; }
.cvo-thread-heads .ppt-comic-name { grid-row: 2; }
/* during the CHOICE, both heads shrink to a small size (neither is emphasised) so the reply chips +
   callout get the room — the learner is choosing, not watching a speaker. */
.cvo-thread.is-deciding .cvo-thread-heads .ppt-comic-side { --comic-head-size: clamp(56px, 15vw, 82px); }
.cvo-thread.is-deciding .cvo-thread-heads { padding-bottom: .8vh; }

@media (prefers-reduced-motion: reduce) {
  .cvo-th-bubble,
  .cvo-thread-chips .cvo-decision-head,
  .cvo-decision.is-resolved .cvo-decision-head,
  .cvo-thread-chips .cvo-comic-option,
  .cvo-thread-chips .cvo-comic-option.is-unchosen,
  .cvo-thread-chips .cvo-comic-option.is-chosen { animation: none; }
  .cvo-thread-chips .cvo-comic-option.is-unchosen { opacity: .4; }   /* still show the resolve, just no motion */
}

/* (The dev-mode one-shot-reveal overrides that used to live here are gone: dev mode no longer freezes
   one-shot animations at all — templates.css now pauses only the perpetual loops — so the comic reveals
   play normally under dev mode and need no override.) */

/* bottom: the scene, aspect-locked and cover-cropped to the character band; bubbles hidden here.
   The crop keys on the CHARACTERS, not the stage's geometric center: --comic-focus-x (set inline
   per scene from the character-wrapper union, ppt-slide.js → comicSceneFocusX) is the % of stage
   width that should sit at the visible center. A per-scene `comicFocusShift` (JSON) can nudge
   it for compositional framing. Translating the stage by -focus puts that point at the center. */
.ppt-comic-scene { flex: 1 1 55%; min-height: 0; position: relative; overflow: hidden; background: #101a34; }
body.responsive .ppt-comic-scene .ppt-stage, html.responsive .ppt-comic-scene .ppt-stage {
  position: absolute; inset: auto; top: 50%; left: 50%;
  transform: translate(calc(-1 * var(--comic-focus-x, 50%)), -50%);
  /* height 108% cover-crops to the character band; --comic-zoom (per-scene JSON) zooms in
     further so a scene with closely-placed characters fills the band instead of leaving gaps. */
  width: auto; height: calc(108% * var(--comic-zoom, 1)); aspect-ratio: 1920 / 821;
}
.ppt-comic-scene .dlg-bubble { display: none; }      /* bubbles live in the panel; the hidden <audio> still plays */
@media (prefers-reduced-motion: reduce) { .ppt-comic-side, .ppt-comic-head { transition: none; } }

/* --- character layers: two wrappers so a pose swap can never move the character (§30.18) ---
   .ppt-char-motion  OUTER: canonical wrapper geometry (union of the deck's authored picture
                     boxes, from characterSlots) + the whole-character entrance transform.
   .ppt-char-slot    INNER: fixed at inset:0 — geometrically stable, never moves on a swap.
   .ppt-pose         every normalized pose canvas, filling the slot exactly. NO object-fit /
                     object-position: each PNG is already composited onto the wrapper, and the
                     canvas aspect equals the wrapper aspect, so 100%x100% is pixel-exact. */
.ppt-char-motion { position: absolute; z-index: 3; pointer-events: none; filter: drop-shadow(0 18px 16px rgba(0,0,0,0.35)); }
.ppt-char-slot { position: absolute; inset: 0; }
.ppt-pose { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* --- live prop layers (`object:<name>` in the deck; authoring/scripts/export_props.py) ---
   Static furniture is baked into the background picture, so these are only the props that
   animate or are interactive. Geometry and rotation come from the deck; the exit beat adds its
   own transform inline. `depth` decides z-index relative to the character group (2 = behind). */
.ppt-prop { position: absolute; display: block; pointer-events: none;
            filter: drop-shadow(0 12px 12px rgba(0,0,0,0.32)); }

/* Character entrance is clock-driven (translateX set inline from currentTime in
   ppt-slide.js), so it replays on scrub/restart. Reduced motion snaps to rest.
   .ppt-beat is exempt: under reduced motion the exit beat does not animate, it hard-cuts to its
   END FRAME — and that frame is a transform, so stripping it would undo the beat entirely. */
@media (prefers-reduced-motion: reduce) {
  .ppt-char-motion:not(.ppt-beat) { transform: none !important; }
}

/* --- dialogue captions (accessibility). Always in the DOM for screen readers;
       visible bar toggled by the player's CC control. --- */
.ppt-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* VISUALLY HIDDEN, BUT NOT WHEN IT HAS FOCUS (author report 2026-07-29: "it shows up when
   I'm hitting tab").

   The transcript is a <details>, and a <summary> is a real interactive control — so it is in
   the tab order, correctly: a screen-reader user needs to reach it. But hidden at 1x1 with
   clip:rect(0 0 0 0), a SIGHTED keyboard user tabs onto a stop they cannot see, with no focus
   ring and nothing to explain where they are. That is WCAG 2.4.7 (Focus Visible), and it is
   the same defect class as the closed chrome panels and the invisible Submit button found in
   the NVDA pass — reachable but invisible.

   So it un-hides itself while focus is inside: the standard skip-link treatment. Nothing
   changes for the mouse, nothing changes for a screen reader, and the keyboard stop becomes
   a visible, readable control. Constrained in height because an expanded transcript is the
   whole scene's script. */
.ppt-sr:focus-within {
  position: absolute; top: 12px; left: 12px; z-index: 40;
  /* max-content, not auto: absolutely-positioned shrink-to-fit collapsed the box to ~36px
     and wrapped the label one word per line. */
  width: max-content; height: auto; max-width: min(70ch, 78%); max-height: 60vh;
  margin: 0; padding: 12px 16px; overflow: auto;
  clip: auto; clip-path: none; white-space: normal;
  background: var(--surface-stage, #0b1633); color: #fff;
  border: 2px solid #fff; border-radius: var(--radius-card, 12px);
  box-shadow: var(--shadow-nav, 0 2px 20px rgba(19, 37, 84, 0.45));
  font-family: var(--font-sans, system-ui, sans-serif); font-size: 16px; line-height: 1.5;
}
.ppt-sr:focus-within summary { cursor: pointer; font-weight: 700; padding: 2px 0; }
.ppt-captions {
  position: absolute; left: 50%; bottom: 24px; transform: translateX(-50%);
  width: min(78%, 1180px); z-index: 6; display: flex; flex-direction: column; gap: 8px;
  background: rgba(10,20,45,0.86); border: 1px solid var(--on-navy-border, rgba(255,255,255,0.5));
  border-radius: var(--radius-card, 16px); padding: 16px 22px; color: #fff;
  font-family: var(--font-sans, system-ui, sans-serif); box-shadow: var(--shadow-nav, 0 2px 20px rgba(19,37,84,0.25));
  animation: mvFadeInSlow 180ms ease;
}
.ppt-caption-line { font-size: 20px; line-height: 1.4; margin: 0; }

/* Comic mode (tablet/phone): the caption box is a SIBLING of the cropped stage (see
   ppt-slide.js → PptSlideView), so it positions against the visible scene area and its edges
   are never cropped. It shows ONE line, sized in real px — the stage here is fluid, not
   transform-scaled like desktop, so the desktop 20px would read oversized on a phone. Capped
   to ~40% of the scene so the scene it captions is never buried. */
.ppt-comic-scene > .ppt-captions {
  left: 8px; right: 8px; bottom: 8px; width: auto; transform: none;
  max-height: 40%; overflow-y: auto; padding: 8px 12px; gap: 4px;
  border-radius: var(--radius-card, 12px);
}
.ppt-comic-scene > .ppt-captions .ppt-caption-line { font-size: clamp(13px, 3.4vw, 17px); line-height: 1.35; }
.ppt-comic-scene > .ppt-captions .ppt-caption-speaker { margin-right: 6px; }
/* TEMPLATE-PAGE caption track (renderers/captions.js, 2026-08-01). Template pages are
   responsive documents, not the fixed 1920x821 stage, so the track pins above the bottom
   chrome instead of positioning inside a stage.
   Capped and scrollable per the author: it must never bury the page it captions, so it takes
   at most ~half the viewport and scrolls inside that cap when a long line overflows — the
   same treatment the comic-mode track already had (`.ppt-comic-scene > .ppt-captions`). */
.ppt-captions--track {
  position: fixed; left: 50%; right: auto; transform: translateX(-50%);
  bottom: calc(var(--rc-footer-h, 96px) + 12px);
  width: min(78%, 1180px);
  max-height: 45vh; overflow-y: auto; overscroll-behavior: contain;
  /* Z-ORDER IS DELIBERATE AND NARROW (author 2026-08-01: captions "should be part of the player
     area", so an open menu covers them).
       40  narration / feedback modal scrims — captions must sit ABOVE these, or a modal that is
           speaking would hide its own captions
       50  THIS
       60  CourseMenu / ResourcesPanel drawers — these must cover the captions
       70  the fixed chrome header
     It was 60, which tied with the drawers; the track is portalled to <body> and therefore comes
     later in document order, so it won the tie and painted over an open menu. */
  z-index: 50;
}
@media (max-width: 1024px) {
  /* tablet/phone: full-bleed within a small inset, and a tighter cap so more of the scene
     stays visible. Scrolling inside the box is what compensates for the smaller height. */
  .ppt-captions--track {
    left: 8px; right: 8px; width: auto; transform: none;
    max-height: 40vh; padding: 8px 12px; gap: 4px;
  }
  .ppt-captions--track .ppt-caption-line { font-size: clamp(13px, 3.4vw, 17px); line-height: 1.35; }
}
/* CC BUTTON ON-STATE (author 2026-08-01). Captions being on was only signalled by a subtle
   change inside the glyph, which is hard to read at a glance — and it matters now that captions
   are being tested against mute. ON draws a light-navy rounded rectangle behind the icon.
   Light navy from the palette (--surface-stage, the raised-panel step) rather than a new colour,
   with a signal-blue hairline so it reads as an active control rather than a disabled one.
   `aria-pressed` carries the same state for assistive tech, so this is not colour-alone. */
/* !important is REQUIRED here, not laziness: the desktop CC button carries the bundle's inline
   `style: ghost` (background + border set on the element), and an inline style beats any class
   rule. Measured — without it aria-pressed flipped correctly while the background stayed
   transparent. The alternative is moving the on-state into the bundle's inline style object,
   which would put a piece of course styling back inside vendored code. */
/* ONE TREATMENT FOR BOTH BOTTOM-BAR TOGGLES (author 2026-08-01: "these need to match").
   The CC button and the settings gear sit side by side, so their "active" square has to be the
   same shape and the same colour. The gear previously used a plain white wash while CC used the
   light-navy + signal-blue square, and side by side they read as two different controls.
   Both now share: same 10px radius, same hover wash, same light-navy ON square with a
   signal-blue hairline. `aria-pressed` (CC) and `aria-expanded` (gear) are the respective
   state attributes, so neither relies on colour alone. */
.mv-cc-btn, .set-cog {
  border-radius: 10px !important;
  border: none !important;
  transition: background 120ms;
}
/* Hover only for devices that HAVE hover. On touch, :hover sticks after a tap until the
   next tap elsewhere — a stuck wash nearly the same shade as the ON square, which read as
   "is it on or not?" at tablet (author, 2026-08-02). Real pointers keep the wash;
   touch gets none, so the only filled state a finger can produce is the real ON state. */
@media (hover: hover) {
  .mv-cc-btn:hover, .set-cog:hover { background: rgba(255,255,255,0.08) !important; }
}
/* The active square is the LIGHT NAVY FILL ONLY — no outline (author 2026-08-01: "I want both
   of them to just look like the light navy square around the gear... I don't want" the signal
   blue outline). State is still carried by aria-pressed / aria-expanded, so dropping the border
   does not make this colour-alone. */
.mv-cc-btn[aria-pressed="true"], .set-cog[aria-expanded="true"], .set-cog.is-open {
  background: var(--surface-stage, #1A2E60) !important;
}
/* ON-state underline (author 2026-08-02): the navy square alone sat too close to the hover
   wash to read as "captions are on", so the ON state additionally carries a signal-blue
   underline bar beneath the glyph — the captions-on convention learners know from video
   players. The gear gets the same mark when its panel is open, so the two controls keep
   reading as one family (the 2026-08-01 matching rule). Hover never shows it, so hover and
   ON cannot be confused at any width. */
/* `position: relative` here exists ONLY to give the ::after underline a containing block, so it
   must not apply to a CC button that is ALREADY positioned. The responsive chrome's `.rc-cc`
   (player.css) is `position: absolute`, centred on the bottom row with translate(-50%,-50%) — and
   screens.css loads after player.css, so an unscoped `relative` won at equal specificity and
   dropped the button into normal flow, where the translate lifted it 22px up onto the scrubber
   (measured, tablet + phone). It is absolute, which is already a containing block for ::after. */
.mv-cc-btn:not(.rc-cc), .set-cog { position: relative; }
.mv-cc-btn[aria-pressed="true"]::after, .set-cog[aria-expanded="true"]::after,
.set-cog.is-open::after {
  content: ""; position: absolute; left: 24%; right: 24%; bottom: 4px; height: 3px;
  border-radius: 2px; background: var(--signal, #00A3DD);
}
.mv-cc-btn:focus-visible { outline: none; box-shadow: var(--focus-ring, 0 0 0 3px rgba(0,163,221,0.55)); }

.ppt-caption-speaker { font-weight: 800; color: var(--signal, #00A3DD); margin-right: 8px; text-transform: uppercase; letter-spacing: 0.4px; }

/* --- hybrid speech bubbles: exact PowerPoint shell (PNG) + live animated text overlay --- */
.dlg-bubble {            /* positioned to the PowerPoint shape box; carries the pop-in */
  position: absolute; z-index: 6;
  animation: mvBubbleIn 300ms cubic-bezier(.2,.7,.3,1) both;
}
.dlg-shell {             /* the exact PPT bubble shell (shape + tail, no text) */
  position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none;
}
.dlg-text {              /* live text, clipped to the original PowerPoint text-box bounds */
  position: absolute; overflow: hidden; display: flex; align-items: center; justify-content: center;
  text-align: center; user-select: none;
  font-family: var(--font-sans, system-ui, sans-serif); font-weight: 600;
  line-height: 1.2; color: var(--ink, #16203A);
}
.dlg-fit { display: block; width: 100%; text-align: center; overflow-wrap: break-word; }
@media (prefers-reduced-motion: reduce) { .dlg-bubble { animation: none !important; } }

/* Conversation Decision (memory: conversation-decision-design). The choice bubbles are real PPT
   shells (same .dlg-bubble hybrid) clustered around the answerer; they PULSE a faint signal-blue
   glow to read as clickable. The container is click-through so only the choices catch the pointer. */
.ppt-conversation { position: absolute; inset: 0; pointer-events: none; z-index: 6; }
.cvo-choice { pointer-events: auto; cursor: pointer; }
.cvo-choice .dlg-shell { animation: cvoPulse 1.8s ease-in-out infinite; }
.cvo-choice:hover .dlg-shell, .cvo-choice:focus-visible .dlg-shell {
  animation: none; filter: drop-shadow(0 0 12px rgba(0, 163, 221, 0.95));
}
.cvo-choice:focus-visible { outline: none; }
@keyframes cvoPulse {
  0%, 100% { filter: drop-shadow(0 0 1px rgba(0, 163, 221, 0.25)); }
  50%      { filter: drop-shadow(0 0 9px rgba(0, 163, 221, 0.85)); }
}
@media (prefers-reduced-motion: reduce) {
  .cvo-choice .dlg-shell { animation: none; filter: drop-shadow(0 0 5px rgba(0, 163, 221, 0.7)); }
}
/* Phase poses (Conversation Decision) crossfade on the PICK — event-driven, so a CSS transition is
   correct here (nothing to scrub); clock-driven poses never carry this class. */
.ppt-pose-phase { transition: opacity 333ms ease; }

/* Local play / replay control — this is the conversation's "play button" (the screen registers no
   global media). Prominent + centered when idle; a small corner replay once it has started. */
.cvo-play {
  position: absolute; left: 50%; bottom: 3%; transform: translateX(-50%);
  z-index: 8; pointer-events: auto; cursor: pointer;
  width: 56px; height: 56px; border-radius: 50%; border: none;
  background: var(--signal, #00A3DD); color: #fff; font-size: 24px; line-height: 1;
  display: flex; align-items: center; justify-content: center; box-shadow: 0 6px 18px rgba(8,12,26,.4);
}
.cvo-play.is-replay { left: auto; right: 2%; transform: none; width: 38px; height: 38px; font-size: 17px; background: rgba(14,28,66,.78); }
.cvo-play:hover { filter: brightness(1.08); }
.cvo-play:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(0,163,221,.55), 0 6px 18px rgba(8,12,26,.4); }
.cvo-comic-play {
  align-self: center; margin-top: .6vh; min-height: 40px; padding: 8px 18px; cursor: pointer;
  border: none; border-radius: 999px; background: var(--signal, #00A3DD); color: #fff;
  font-weight: 700; font-size: clamp(13px, 3.4vw, 15px); box-shadow: 0 4px 12px rgba(8,12,26,.35);
}
.cvo-comic-play:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(0,163,221,.5); }

/* --- seekable narration timeline (footer) --- */
.media-scrubber { position: absolute; top: 0; left: 0; right: 0; height: 14px; cursor: pointer; z-index: 2; touch-action: none; }
.media-scrubber-track { position: absolute; left: 0; right: 0; top: 0; height: 5px; background: rgba(255,255,255,0.16); }
.media-scrubber-fill { position: absolute; left: 0; top: 0; height: 5px; background: var(--signal, #00A3DD); }
.media-scrubber-thumb { position: absolute; top: -4px; width: 14px; height: 14px; border-radius: 50%; background: var(--signal, #00A3DD); transform: translateX(-50%); box-shadow: 0 1px 4px rgba(0,0,0,0.45); }
.media-scrubber:hover .media-scrubber-thumb { transform: translateX(-50%) scale(1.18); }
.media-scrubber:focus-visible { outline: none; }
.media-scrubber:focus-visible .media-scrubber-thumb { box-shadow: 0 0 0 4px rgba(0,163,221,0.6); }
/* observe-only (rewind) scrubbing: the not-yet-watched stretch AHEAD of the high-water mark
   is dimmed and can't be seeked into (forward scrubbing is capped there; only backward works).
   `left` is set inline to the watched fraction; the region runs to the track end. */
.media-scrubber-locked { position: absolute; right: 0; top: 0; height: 5px;
  background: repeating-linear-gradient(45deg, rgba(255,255,255,0.06) 0 4px, rgba(255,255,255,0.16) 4px 8px); }
/* the decision-gate lock: an amber notch marking where the learner must decide */
.media-scrubber-lock { position: absolute; top: -4px; width: 3px; height: 13px; border-radius: 2px; z-index: 3;
  background: #FFD23F; box-shadow: 0 0 0 1.5px var(--surface-chrome, #0E1C42); transform: translateX(-50%); }

/* --- settings menu: gear button + audio/transition panel ---------------------
   Replaces the old `.vol` hover-to-open speaker slider (removed 2026-08-01 with
   VolumeControl in player.js). Shared by the desktop chrome and ResponsiveShell, so
   these rules are the only place the control is styled. Colours, radii and the focus
   ring all come from the player's existing tokens — no new visual vocabulary. */
.set-wrap { position: relative; display: inline-flex; align-items: center; justify-content: center; }

/* The rounded-rectangle background is BACK; the rotation is not (author 2026-08-01).
   These are independent properties, so the button can acknowledge the pointer exactly like
   the rest of the chrome while still staying visually upright when clicked — the tilt was
   the part that was wrong, not the hover box.
   `transform: none` is asserted on every interactive state rather than simply omitted, so a
   later `:active { transform: scale(...) }` copied from the other chrome buttons cannot
   quietly reintroduce a press-tilt. */
.set-cog {
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; border: none; border-radius: 10px; background: transparent;
  color: var(--signal, #00A3DD); cursor: pointer;
  transition: background 120ms;
}
/* Hover / open backgrounds now come from the SHARED rule with .mv-cc-btn (see the block by
   .ppt-caption-speaker) so the gear and the CC button cannot drift apart again. Only the
   pressed wash stays local, because CC has no equivalent momentary state. */
.set-cog:active { background: rgba(255,255,255,0.14); }
.set-cog, .set-cog:hover, .set-cog:active, .set-cog.is-open,
.set-cog:hover:active, .set-cog[aria-expanded="true"] { transform: none; }
.set-cog:focus-visible { outline: none; box-shadow: var(--focus-ring, 0 0 0 3px rgba(0,163,221,0.55)); }

.set-panel {
  position: absolute; bottom: 100%; right: 0; margin-bottom: 10px;
  width: 300px; max-width: calc(100vw - 32px);
  background: rgba(14,28,66,0.98); border: 1px solid rgba(255,255,255,0.14);
  border-radius: 12px; padding: 14px; box-shadow: 0 8px 22px rgba(0,0,0,0.45);
  opacity: 0; pointer-events: none; transition: opacity 130ms ease; z-index: 40;
  font: 600 13px/1.3 'Archivo', system-ui, sans-serif; color: #fff;
  display: flex; flex-direction: column; gap: 12px;
}
.set-panel.is-open { opacity: 1; pointer-events: auto; }

.set-row { display: flex; flex-direction: column; gap: 6px; }
.set-row-label { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; cursor: pointer; }
.set-row-name { font-weight: 700; }
.set-row-value { font-variant-numeric: tabular-nums; opacity: 0.75; font-weight: 700; }

/* 44px of vertical hit area on touch without a 44px-tall visual bar: the padding is
   part of the control, so the thumb stays easy to grab on a phone. */
.set-slider {
  width: 100%; margin: 0; padding: 12px 0; background: transparent;
  accent-color: var(--signal, #00A3DD); cursor: pointer; touch-action: none;
}
.set-slider:focus-visible { outline: none; box-shadow: var(--focus-ring, 0 0 0 3px rgba(0,163,221,0.55)); border-radius: 8px; }

/* Every row is [mute icon][slider]. The mute sits to the LEFT of its own slider, so the
   control that silences a channel is adjacent to the one that sets its level. */
.set-row-controls { display: flex; align-items: center; gap: 10px; }
.set-row-controls .set-slider { flex: 1 1 auto; min-width: 0; }

/* ICON ONLY — no caption. The state is carried by the glyph, aria-pressed, the accessible
   name and the tooltip (author 2026-08-01: no persistent visible text). 44px tap target. */
.set-mute {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0;
  border: none; background: transparent; cursor: pointer;
  color: var(--signal, #00A3DD);
}
.set-mute:focus-visible { outline: none; box-shadow: var(--focus-ring, 0 0 0 3px rgba(0,163,221,0.55)); border-radius: 8px; }
/* Muted uses the palette's own INCORRECT/warning red (tokens.css --incorrect), not a colour
   invented for this control. The earlier amber was not in the style guide at all — the course
   has never used yellow outside the radiation ILLUSTRATION tokens, which are artwork, not UI.
   State is not carried by colour alone: the glyph itself changes to a crossed-out speaker. */
.set-mute.is-muted { color: var(--incorrect, #E5463E); }
/* Silenced only because MASTER is muted: dimmed, still showing its own un-muted glyph, so it
   never claims this button is the one holding the channel down. */
.set-mute.is-by-master { opacity: 0.45; }
.set-slider.is-silent { opacity: 0.45; }

/* Carousel Transition: a real checkbox (role=switch) with the pill drawn over it. The
   input keeps its own focus ring and keyboard behaviour; only its box is hidden. */
.set-switch-row { padding-top: 2px; border-top: 1px solid rgba(255,255,255,0.12); }
.set-switch { display: flex; align-items: center; justify-content: space-between; gap: 10px; cursor: pointer; min-height: 44px; }
.set-switch-controls { position: relative; display: inline-flex; align-items: center; gap: 8px; }
.set-switch-input { position: absolute; inset: 0; width: 46px; height: 26px; margin: 0; opacity: 0; cursor: pointer; }
.set-switch-pill {
  position: relative; display: inline-block; width: 46px; height: 26px; border-radius: 13px;
  background: rgba(255,255,255,0.22); border: 1px solid rgba(255,255,255,0.28);
  transition: background 140ms, border-color 140ms;
}
.set-switch-knob {
  position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%;
  background: #fff; transition: transform 140ms cubic-bezier(.2,.7,.3,1);
}
.set-switch-input:checked + .set-switch-pill { background: var(--signal, #00A3DD); border-color: var(--signal, #00A3DD); }
.set-switch-input:checked + .set-switch-pill .set-switch-knob { transform: translateX(20px); }
.set-switch-input:focus-visible + .set-switch-pill { box-shadow: var(--focus-ring, 0 0 0 3px rgba(0,163,221,0.55)); }
.set-switch-state { min-width: 26px; font-weight: 700; opacity: 0.85; }

/* Pass-through slot (ResponsiveShell puts the captions toggle here). */
.set-extra-row { padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.12); }
.set-pill-btn {
  display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px;
  padding: 8px 12px; border: 1px solid rgba(255,255,255,0.2); border-radius: 8px;
  background: rgba(255,255,255,0.08); color: #fff; cursor: pointer;
  font: 700 13px/1 'Archivo', system-ui, sans-serif; text-align: left;
}
.set-pill-btn[aria-pressed="true"] { background: rgba(0,163,221,0.28); border-color: var(--signal, #00A3DD); }
.set-pill-btn:focus-visible { outline: none; box-shadow: var(--focus-ring, 0 0 0 3px rgba(0,163,221,0.55)); }

/* Narrow screens: the panel is anchored to the gear, which sits in the bottom bar, so
   it must never push past either viewport edge. */
@media (max-width: 600px) {
  .set-panel { width: min(300px, calc(100vw - 24px)); right: 50%; transform: translateX(50%); }
}

@media (prefers-reduced-motion: reduce) {
  .set-switch-knob, .set-panel { transition: none; }
}

/* --- interactive overlays: hotspots + choice buttons --- */
.ppt-overlay { position: absolute; inset: 0; z-index: 5; }
.ppt-hotspot {
  position: absolute; border: 2px dashed rgba(0,163,221,0.9); background: rgba(0,163,221,0.10);
  border-radius: var(--radius-sm, 8px); cursor: pointer; min-width: var(--hit-target, 44px); min-height: var(--hit-target, 44px);
}
.ppt-hotspot:focus-visible, .ppt-choice:focus-visible { outline: none; box-shadow: var(--focus-ring, 0 0 0 3px rgba(0,163,221,0.55)); }

/* choice panel: readable HTML buttons over the slide */
.ppt-choices {
  position: absolute; right: 40px; top: 50%; transform: translateY(-50%); z-index: 6;
  width: 40%; max-width: 640px; display: flex; flex-direction: column; gap: 13px;
  background: rgba(19,37,84,0.92); border: 1px solid var(--on-navy-border, rgba(255,255,255,0.5));
  border-radius: var(--radius-card, 16px); padding: 28px; animation: mvSlide 220ms cubic-bezier(.2,.7,.3,1);
}
.ppt-prompt { font-family: var(--font-sans); font-weight: 800; font-size: 26px; color: #fff; line-height: 1.2; margin: 0 0 6px; }
.ppt-choice {
  display: flex; align-items: center; gap: 14px; width: 100%; text-align: left; cursor: pointer;
  min-height: var(--hit-target, 44px); padding: 16px 18px; border-radius: var(--radius-option, 12px);
  border: var(--border-control, 1.5px) solid var(--border, #DCE2EC); background: #fff; color: var(--ink, #16203A);
  font-family: var(--font-sans); font-weight: 600; font-size: 18px; transition: background var(--dur-fast,120ms) ease, border-color var(--dur-fast,120ms) ease;
}
.ppt-choice:hover:not(:disabled) { background: var(--signal-wash, rgba(0,163,221,0.07)); }
.ppt-choice[aria-pressed="true"] { border-color: var(--signal, #00A3DD); border-width: var(--border-active, 2px); }
.ppt-choice[data-state="correct"] { border-color: var(--correct, #12A66A); background: var(--correct-wash, rgba(18,166,106,0.08)); }
.ppt-choice[data-state="incorrect"] { border-color: var(--incorrect, #E5463E); background: var(--incorrect-wash, rgba(229,70,62,0.07)); }
.ppt-choice .ppt-letter { flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; background: var(--page, #EEF1F6); color: var(--navy, #132554); }
.ppt-choice:disabled { cursor: default; }

/* inline (non-modal) feedback — icon + label + text, never color alone */
.ppt-feedback {
  display: flex; gap: 12px; align-items: flex-start; margin-top: 6px; padding: 14px 16px;
  border-radius: var(--radius-option, 12px); border: 2px solid; animation: mvPop 320ms cubic-bezier(.34,1.56,.64,1);
}
.ppt-feedback[data-kind="correct"] { border-color: var(--correct, #12A66A); background: var(--correct-wash, rgba(18,166,106,0.08)); }
.ppt-feedback[data-kind="incorrect"] { border-color: var(--incorrect, #E5463E); background: var(--incorrect-wash, rgba(229,70,62,0.07)); }
.ppt-feedback .ppt-fb-badge { flex: 0 0 auto; width: 28px; height: 28px; border-radius: 50%; color: #fff; font-weight: 800; display: flex; align-items: center; justify-content: center; }
.ppt-feedback[data-kind="correct"] .ppt-fb-badge { background: var(--correct, #12A66A); }
.ppt-feedback[data-kind="incorrect"] .ppt-fb-badge { background: var(--incorrect, #E5463E); }
.ppt-fb-title { font-weight: 800; color: #fff; font-size: 17px; margin-bottom: 2px; }
.ppt-fb-body { color: rgba(255,255,255,0.85); font-size: 15px; line-height: 1.45; }
