/* ==========================================================================
   lts-scene.css - motion scene figures (.lts-scene)

   Scenes are authored in viz/ (Remotion) and render on the page as live SVG from
   the very same components. This file styles the FIGURE AROUND the scene - the
   stage, the step chrome, the captions. It never styles the scene's interior:
   that is the SVG's own business, and reaching into it from here would be a way
   for the page and the video to drift apart.

   DELIBERATELY NOT BUILT ON .iqv-plate. Three reasons, all load-bearing:
     1. The Transit skin sets `zoom: 1.16` on .iqv-plate
        (interview-questions-v2.css:4153). CSS zoom splits visual from layout
        pixels - iqv-connectors.js:45 exists solely to normalise it - and that
        would fight the Remotion-vs-browser pixel diff the whole design rests on.
     2. It would drag in a 160KB stylesheet that owns every .iqv-* rule, on pages
        that need only a scene.
     3. The iqv reveal engine veils [data-iqv-stage] elements; a scene instead
        drives a clock. The mechanism does not transfer.

   Tokens come from instrument.css (--in-*), the same sheet viz/ generates its
   token module from, so the figure and the scene inside it agree by construction.
   ========================================================================== */

.lts-scene {
   margin: 28px 0;
   padding: 0;
   border: 1px solid var(--in-hairline, #C9C7C0);
   border-radius: 14px;
   background: var(--in-panel, #E9E8E3);
   box-shadow: var(--in-lift, 0 10px 24px -16px rgba(35, 35, 32, .35));
   overflow: hidden;
}

/* The mount host is what the runtime replaces wholesale. It deliberately carries
   no aspect-ratio: that belongs on the stage inside it, or it would constrain the
   scene and the step chrome together once mounted and squash the figure. */
.lts-scene-mount {
   display: block;
}

/* The stage carries an inline `aspect-ratio` - from the server before hydration,
   from the runtime after - so its height is reserved before anything paints and
   swapping one for the other moves nothing. */
.lts-scene-stage {
   display: block;
   width: 100%;
   position: relative;
}

/* Brand imprint - ONE faint "learnthatstack.com" centred on the stage so a
   cropped screenshot still carries the mark (2026-08-01, user ask). Re-worked
   2026-08-20 from the old 440x320 tile: the lattice repeated a dozen times per
   frame, and in fullscreen the stage letterboxes the SVG, so tiles painted the
   empty margins OUTSIDE the drawn frame too. A single centred mark sits where
   the frame actually is (letterboxing is symmetric) and reads once, larger.
   Sits on the stage, not .lts-scene, so it covers the figure - the
   server-rendered SVG poster before hydration and the live scene after - and
   never the caption or step chrome. Sized relative to the stage so compact
   embeds get a proportionally smaller mark. pointer-events none keeps the live
   scene's keyboard/click handling untouched. (.iqv-plate keeps its own tile in
   interview-questions-v2.css.) */
.lts-scene-stage::after {
   content: '';
   position: absolute;
   inset: 0;
   background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='560' height='140'%3E%3Ctext x='280' y='80' text-anchor='middle' transform='rotate(-8 280 70)' font-family='monospace' font-size='36' fill='%2394938B'%3Elearnthatstack.com%3C/text%3E%3C/svg%3E");
   background-repeat: no-repeat;
   background-position: center;
   background-size: min(70%, 640px) auto;
   opacity: .06;
   mix-blend-mode: multiply;
   pointer-events: none;
}

/* The flagship stage theme is DARK (scene-stage.css --sc-stage: #1B1917), where
   a multiplied dark-gray mark computes to nothing. Same mark, but lifted out of
   the dark surface with screen - mirrors the scene share cards' treatment.
   Tuning carried over from the tile era (2026-08-11): the mark should survive a
   levels inspection of a cropped screenshot, not compete with the scene. */
.lts-scene[data-scene-theme="dark"] .lts-scene-stage::after {
   opacity: .04;
   mix-blend-mode: screen;
}

.lts-scene-stage > svg,
.lts-scene-live svg {
   display: block;
   width: 100%;
   height: 100%;
}

.lts-scene-live {
   display: block;
   outline: none;
}

/* Keyboard focus must be visible: the figure is operable with arrow keys, so a
   keyboard user has to be able to tell when it has focus. */
.lts-scene-live:focus-visible {
   outline: 2px solid var(--in-orange, #E55A12);
   outline-offset: -2px;
   border-radius: 12px;
}

/* --------------------------------------------------------------------------
   Captions
   -------------------------------------------------------------------------- */

.lts-scene-caption {
   padding: 14px 18px 16px;
   border-top: 1px solid var(--in-hairline, #C9C7C0);
   background: var(--in-well, #F8F7F4);
}

.lts-scene-title {
   margin: 0 0 6px;
   font-family: var(--in-sans, 'Instrument Sans', system-ui, sans-serif);
   font-size: 14px;
   font-weight: 600;
   color: var(--in-ink2, #45443E);
}

/* The full step list is the figure's content when JS never arrives. It is also
   the only part of a scene a search engine can read, so it is real text in the
   document rather than anything generated at runtime. */
.lts-scene-steps {
   margin: 0;
   padding-left: 20px;
   font-family: var(--in-sans, 'Instrument Sans', system-ui, sans-serif);
   font-size: 14.5px;
   line-height: 1.55;
   color: var(--in-ink3, #67665F);
}

.lts-scene-steps li + li {
   margin-top: 4px;
}

/* Once the runtime takes over, the whole static caption block is duplicate text:
   the chrome shows a live caption for the current step, and the scene draws its
   own headline. Hidden only when actually MOUNTED - if the runtime fails to load
   or is blocked, this stays and is the figure's entire content. */
.lts-scene[data-lts-mounted] .lts-scene-caption {
   display: none;
}

/* --------------------------------------------------------------------------
   Step chrome (runtime-rendered)
   -------------------------------------------------------------------------- */

.lts-scene-chrome {
   display: flex;
   align-items: center;
   gap: 16px;
   padding: 12px 18px 14px;
   border-top: 1px solid var(--in-hairline, #C9C7C0);
   background: var(--in-well, #F8F7F4);
   /* Anchor for the autoplay progress hairline. */
   position: relative;
}

/* Autoplay progress: a 2px hairline riding the chrome's top border, filled by the
   playhead. The only always-on trace that a figure is in auto mode - restrained on
   purpose, the same register as the iqv [data-iqv-fill] bar. Width is driven per
   animation frame by the runtime, so it needs (and must not have) a CSS transition. */
.lts-scene-progress {
   position: absolute;
   top: -1px;
   left: 0;
   right: 0;
   height: 2px;
   background: transparent;
   pointer-events: none;
}

.lts-scene-progress-fill {
   height: 100%;
   background: var(--in-orange, #E55A12);
}

/* Horizontal swipes step the scene; vertical gestures stay the page's. Scoped to the
   mounted stage - the pre-hydration poster has nothing to swipe. */
.lts-scene-live .lts-scene-stage {
   touch-action: pan-y;
}

/* Brand chrome - FULLSCREEN ONLY. Fullscreen is the view people screenshot and
   share, and its bar has an empty left end; inline embeds keep the quiet bar
   (the page around them already carries the header brand and the (c) line).
   Lives in the bar, never the stage - the stage is governed by the
   one-protagonist rules and gets no permanent chrome. aria-hidden in the
   markup: decorative branding, not content. */
.lts-scene-brand {
   display: none;
   align-items: center;
   gap: 8px;
   flex: 0 0 auto;
}

.lts-scene.is-fullscreen .lts-scene-brand {
   display: inline-flex;
}

.lts-scene-brand img {
   display: block;
   width: 34px;
   height: auto;
}

.lts-scene-brand__word {
   font-family: var(--in-sans, 'Instrument Sans', system-ui, sans-serif);
   font-size: 13px;
   font-weight: 600;
   letter-spacing: .01em;
   color: var(--in-ink2, #45443E);
   white-space: nowrap;
}

.lts-scene-brand__word b {
   font-weight: 600;
   color: var(--in-orange, #E55A12);
}

.lts-scene-narr {
   flex: 1 1 auto;
   min-width: 0;
}

.lts-scene-narr p {
   margin: 0;
   font-family: var(--in-sans, 'Instrument Sans', system-ui, sans-serif);
   font-size: 14.5px;
   line-height: 1.5;
   color: var(--in-ink2, #45443E);
}

/* The close stop's kicker, inline: the same mark the fullscreen context line
   and the question page's band use, at the bar's scale. The closing prose
   itself reads in the caption's place at the caption's size - a stop of its
   own, not a second paragraph under the caption. */
.lts-scene-narr p.lts-scene-coda-k {
   margin: 0 0 3px;
   font-family: var(--in-mono, 'Fragment Mono', ui-monospace, monospace);
   font-size: 10.5px;
   line-height: 1.4;
   letter-spacing: .13em;
   text-transform: uppercase;
   color: var(--in-orange, #E55A12);
}

.lts-scene-narr p.lts-scene-coda + p.lts-scene-coda {
   margin-top: .55em;
}



.lts-scene-bar {
   display: flex;
   align-items: center;
   gap: 10px;
   flex: 0 0 auto;
}

.lts-scene-btn {
   width: 34px;
   height: 34px;
   display: inline-flex;
   align-items: center;
   justify-content: center;
   border: 1px solid var(--in-hairline, #C9C7C0);
   border-radius: 8px;
   background: var(--in-panel, #E9E8E3);
   color: var(--in-ink, #232320);
   font-size: 15px;
   line-height: 1;
   cursor: pointer;
}

.lts-scene-btn:hover:not(:disabled) {
   border-color: var(--in-orange, #E55A12);
   color: var(--in-orange, #E55A12);
}

.lts-scene-btn:disabled {
   opacity: .4;
   cursor: default;
}

.lts-scene-count {
   font-family: var(--in-mono, 'Fragment Mono', ui-monospace, monospace);
   font-size: 13px;
   color: var(--in-ink3, #67665F);
   font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   Scroll mode

   The figure becomes a tall track with a sticky stage: the scene holds still on
   screen while the reader scrolls past it, and their scroll position drives the
   playhead. Suits long-form reading, where a diagram should advance with the
   prose rather than ask to be clicked.

   Everything here is gated on [data-lts-mounted], so without JavaScript the
   figure stays an ordinary static one. Applying the tall track unconditionally
   would leave a multi-viewport hole in the article for any reader whose runtime
   never arrives.

   The track length sets the pace: 220vh per step is roughly a comfortable read.
   -------------------------------------------------------------------------- */

.lts-scene[data-scene-mode="scroll"][data-lts-mounted] {
   height: var(--lts-scroll-track, 320vh);
   position: relative;
   overflow: visible;
   background: none;
   border: 0;
   box-shadow: none;
}

.lts-scene[data-scene-mode="scroll"][data-lts-mounted] .lts-scene-mount {
   position: sticky;
   /* Leaves room above for the reader's place in the prose, and below for chrome. */
   top: 8vh;
   max-height: 84vh;
}

.lts-scene[data-scene-mode="scroll"][data-lts-mounted] .lts-scene-live {
   border: 1px solid var(--in-hairline, #C9C7C0);
   border-radius: 14px;
   overflow: hidden;
   background: var(--in-panel, #E9E8E3);
   box-shadow: var(--in-lift, 0 10px 24px -16px rgba(35, 35, 32, .35));
}

.lts-scene[data-scene-mode="scroll"][data-lts-mounted] .lts-scene-stage {
   max-height: 66vh;
   margin: 0 auto;
}

/* Fullscreen must win over the sticky track, or the figure would stay pinned
   inside its own scroll container instead of covering the viewport. */
.lts-scene[data-scene-mode="scroll"].is-fullscreen {
   height: 100%;
}

.lts-scene[data-scene-mode="scroll"].is-fullscreen .lts-scene-mount {
   position: static;
   max-height: none;
   top: auto;
}

/* The track's 66vh cap is for a stage sharing the page with prose; fullscreen
   has the whole window and the column rule below decides the size. */
.lts-scene[data-scene-mode="scroll"].is-fullscreen .lts-scene-stage {
   max-height: 100%;
   margin: auto;
}

/* --------------------------------------------------------------------------
   Fullscreen

   Pseudo-fullscreen rather than the native Fullscreen API, which iOS Safari does
   not support for anything but <video> - and phones are exactly where a wide
   diagram most needs the room. Only the figure's class changes, so the runtime's
   React root is never moved and the playhead and step survive by construction.
   -------------------------------------------------------------------------- */

html.lts-lock {
   overflow: hidden;
}

/* A host carrying fullscreen from one figure to the next (chapters of one
   concept are separate figures) leaves the viewport uncovered for the frames
   between the two - long enough to flash the page underneath. This holds the
   dark room over it. Sits BELOW the figure's own 9999 so the arriving
   fullscreen paints on top of it. */
html.lts-fs-hold::before {
   content: '';
   position: fixed;
   inset: 0;
   z-index: 9998;
   background: var(--lts-fs-hold-bg, #24211E);
}

/*
 * ONE surface. The backdrop is the scene's OWN outer bezel colour, so the
 * matting every scene canvas draws around its stage (44px at the house 1200x660)
 * merges into the room and the lit stage is the only object on screen. It also
 * makes letterboxing invisible: when a caption runs long and the stage box is
 * capped by height, the empty columns are the same colour as the bezel beside
 * them. Falls back to the panel where scene-stage.css is not loaded.
 */
.lts-scene.is-fullscreen {
   position: fixed;
   inset: 0;
   z-index: 9999;
   margin: 0;
   border: 0;
   border-radius: 0;
   box-shadow: none;
   background: var(--sc-bezel, var(--in-panel, #E9E8E3));
   display: flex;
   flex-direction: column;
   padding-bottom: 14px;
}

.lts-scene.is-fullscreen .lts-scene-mount {
   flex: 1 1 auto;
   min-height: 0;
   display: flex;
}

/*
 * ONE column. The narration, the figure and the bar are all the width of the
 * stage, so they hang off the same two edges - the alignment the old chrome had
 * three of. The width is derived, not measured: --lts-ar is the scene's own
 * canvas ratio (written by the runtime on this element), and what the chrome
 * leaves of the viewport height decides how wide the stage can be. So it is
 * right for a 1200x660 wide canvas and a 390x520 compact one alike, with no
 * JavaScript in the loop.
 */
.lts-scene.is-fullscreen .lts-scene-live {
   --lts-gut: clamp(18px, 3vw, 56px);
   --lts-bar-h: 58px;
   /* What the narration block reserves, spelled out: its top padding, the
      kicker line + the gaps, and THREE lines of caption at whatever size the
      caption is showing. Every term is the same expression the rules below
      use, so the stage takes exactly the room that is left and no more. */
   --lts-narr-h: calc(clamp(18px, 3.4vh, 34px) + 35px + 4.65 * clamp(16px, 0.9vw + 9px, 21px));
   --lts-col: min(
      calc(100% - 2 * var(--lts-gut)),
      calc((100dvh - var(--lts-narr-h) - var(--lts-bar-h) - 16px) * var(--lts-ar, 1.82))
   );
   flex: 1 1 auto;
   min-width: 0;
   min-height: 0;
   display: flex;
   flex-direction: column;
}

/* The figure is focused on entering fullscreen so the arrow keys work; a ring
   drawn around the whole viewport says nothing (there is nothing else on screen
   for it to be distinguished from) and reads as a stray border. The bar names
   the keys instead, and every button inside keeps its own focus ring. */
.lts-scene.is-fullscreen .lts-scene-live:focus-visible {
   outline: none;
}

/* auto margins absorb the slack: centred horizontally, and whatever height the
   narration and the bar leave over is split evenly above and below. */
.lts-scene.is-fullscreen .lts-scene-stage {
   flex: 0 1 auto;
   /* min-height:0 or the aspect-ratio box is its own minimum and refuses to
      shrink: a caption that runs past its reserve would push the bar off the
      bottom of the screen instead. Shrinking letterboxes the scene inside a
      slightly wider box, which is invisible - the backdrop is the same colour
      as the bezel beside it. */
   min-height: 0;
   width: min(100%, var(--lts-col));
   max-height: 100%;
   margin: auto;
}

.lts-scene.is-fullscreen .lts-scene-chrome {
   flex: 0 0 auto;
   width: min(100%, var(--lts-col));
   min-height: var(--lts-bar-h);
   margin: 0 auto;
   padding: 0;
   gap: 18px;
   border: 0;
   background: none;
}

/* The caption has left the bar for the band above; keep the stepper at the
   bar's right end, where it was, instead of huddled against the brand. */
.lts-scene.is-fullscreen .lts-scene-bar {
   margin-left: auto;
}

/* The bar's middle slot: the keys and Escape have no visible affordance in
   fullscreen, so they are named - unless the host has lent a way on, in which
   case the same slot names where it leads. */
.lts-scene-slot {
   flex: 1 1 auto;
   min-width: 0;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   text-align: center;
   font-family: var(--in-mono, 'Fragment Mono', ui-monospace, monospace);
   font-size: 11.5px;
   letter-spacing: .04em;
   color: var(--in-ink3, #67665F);
}

/* A labelled control in the bar - the host's "Next chapter" and its mate. */
.lts-scene-btn-text {
   width: auto;
   gap: 7px;
   padding: 0 14px;
   font-family: var(--in-sans, 'Instrument Sans', system-ui, sans-serif);
   font-size: 13.5px;
   font-weight: 600;
   max-width: 42vw;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
}

.lts-scene-power {
   border-color: #B8440A;
   background: linear-gradient(180deg, #F0702E, #DE5510);
   box-shadow: 0 1px 0 rgba(255, 255, 255, .25) inset, 0 8px 18px -10px rgba(229, 90, 18, .8);
   color: #fff;
}

.lts-scene-power:hover:not(:disabled) {
   border-color: #B8440A;
   background: linear-gradient(180deg, #E9661F, #C24A0B);
   color: #fff;
}

/* Fullscreen narration: its own block ABOVE the stage, at reading size, on the
   same surface as everything else - a lesson slide reads narration first,
   figure second, stepper last. The height is RESERVED (--lts-narr-h): captions
   run one line to three, and a block that sized itself to each of them would
   move the figure on every step. */
.lts-scene-narr-top {
   flex: 0 0 auto;
   width: min(100%, var(--lts-col));
   min-height: var(--lts-narr-h);
   margin: 0 auto;
   padding: clamp(18px, 3.4vh, 34px) 0 10px;
   border: 0;
   background: none;
}

/* Three lines are RESERVED on the caption itself, so the block is the same
   height for any caption in the one-to-three line range and the figure below
   does not move as the reader steps. Measured against the corpus at 1440x900:
   18 of this concept's 19 captions are two or three lines. A longer one still
   pushes the figure down and shrinks it - reserving four lines instead would
   cost every reader ~5% of the figure to hold the last case still, which is
   the wrong trade. (--lts-narr-h above is the same reserve, expressed for the
   column calc; it only has to be close.) */
.lts-scene-narr-top .lts-scene-read {
   /* In the CAPTION's own size, not the wrapper's em - the wrapper inherits
      the figure's base size, and three lines of that are two of the caption. */
   min-height: calc(3 * 1.55 * clamp(16px, 0.9vw + 9px, 21px));
}



.lts-scene-narr-top p {
   max-width: 76ch;
   margin: 0;
   font-family: var(--in-sans, 'Instrument Sans', system-ui, sans-serif);
   font-size: clamp(16px, 0.9vw + 9px, 21px);
   line-height: 1.55;
   color: var(--in-ink, #232320);
   text-wrap: pretty;
}

/* Where this scene sits in the host's larger walk ("Chapter 1 of 3"). Fullscreen
   leaves the page behind, and with it every trace of that. */
.lts-scene-narr-top p.lts-scene-context {
   margin-bottom: 9px;
   font-family: var(--in-mono, 'Fragment Mono', ui-monospace, monospace);
   font-size: 11.5px;
   line-height: 1.4;
   letter-spacing: .13em;
   text-transform: uppercase;
   color: var(--in-orange, #E55A12);
}

/* The close stop: the narration's closing beats, bound to no step, read in the
   caption's place one stop past the last step (concept-module.js lends them,
   data-lts-closing; the runtime owns the stop). Same size and reserve as a
   caption - it IS the read line at that stop - so the figure holds still on
   the way in and out. Two beats stack with a breath between. */
.lts-scene-narr-top p.lts-scene-coda + p.lts-scene-coda {
   margin-top: .55em;
}


/* Phones: the brand and the middle slot give up their room to the controls,
   and the way on takes what it needs of a full-width bar. */
@media (max-width: 700px) {
   .lts-scene.is-fullscreen .lts-scene-brand,
   .lts-scene.is-fullscreen .lts-scene-slot {
      display: none;
   }

   .lts-scene.is-fullscreen .lts-scene-bar {
      width: 100%;
      margin: 0;
   }

   /* One row of equal controls: a 34px stepper beside a 40px labelled button
      reads as two different kinds of thing, and 34px is a small target where
      these are the only controls on screen. */
   .lts-scene.is-fullscreen .lts-scene-btn {
      width: 40px;
      height: 40px;
   }

   .lts-scene.is-fullscreen .lts-scene-btn-text {
      flex: 1 1 auto;
      min-width: 0;
      max-width: none;
      height: 40px;
      justify-content: center;
   }
}

/* --------------------------------------------------------------------------
   Layout selection before hydration

   A wide scene scaled to a phone turns 15px type into roughly 4px - unreadable
   rather than merely small - so scenes author a separate `compact` canvas at
   phone size. The server emits a poster for EVERY layout the scene declares and
   these rules pick one, because a media query is the only mechanism available
   before any JavaScript runs.

   After hydration the runtime chooses by CONTAINER width instead (see
   useResponsiveLayout in viz/src/web/mount.tsx), which also catches a figure in a
   narrow column on a wide screen - something a media query cannot see. The 640px
   breakpoint is shared with COMPACT_MAX_WIDTH there so the two agree.

   A scene with only a `wide` poster keeps showing it; scaled is better than absent.
   -------------------------------------------------------------------------- */

.lts-scene-stage[data-lts-poster="compact"] {
   display: none;
}

@media (max-width: 640px) {
   /* Hide wide ONLY when a compact poster exists to replace it - :has() lets the
      rule ask that question without the server having to mark the pair. */
   .lts-scene-mount:has(> [data-lts-poster="compact"]) > [data-lts-poster="wide"] {
      display: none;
   }

   .lts-scene-stage[data-lts-poster="compact"] {
      display: block;
   }

   .lts-scene-chrome {
      flex-wrap: wrap;
      gap: 10px;
   }
}

/* Browsers without :has() keep the wide poster on small screens - scaled and
   cramped, but present and correct. Never a blank figure. */

/* --------------------------------------------------------------------------
   In-prose step references

   A markdown link whose target is `#scene-<id>-step-N` drives the figure to that
   step (runtime.js delegates the click). Styled as a small chip so "watch step 4"
   reads as a control rather than a navigation link - and because the selector keys
   on the href alone, authors write a plain link and get the affordance for free.
   With JS off the same link is an ordinary anchor to the figure, whose caption list
   contains step N's text; nothing is lost.
   -------------------------------------------------------------------------- */

a[href^="#scene-"] {
   display: inline-block;
   padding: 0 7px 1px;
   border: 1px solid var(--in-hairline, #C9C7C0);
   border-radius: 6px;
   background: var(--in-well, #F8F7F4);
   font-family: var(--in-mono, 'Fragment Mono', ui-monospace, monospace);
   font-size: .86em;
   color: var(--in-ink2, #45443E);
   text-decoration: none;
   white-space: nowrap;
}

a[href^="#scene-"]:hover {
   border-color: var(--in-orange, #E55A12);
   color: var(--in-orange, #E55A12);
}

/* --------------------------------------------------------------------------
   Print + reduced motion
   -------------------------------------------------------------------------- */

@media print {
   .lts-scene-chrome {
      display: none;
   }

   /* Restore the full caption block on paper: nobody can step through a printed
      figure, so every caption has to be visible at once. */
   .lts-scene[data-lts-mounted] .lts-scene-caption {
      display: block;
   }
}

/* The runtime is the primary enforcement - it snaps tweens to rest frames and
   disables autoplay - but CSS-side motion must honour the setting independently, so
   a runtime that never loads (or a future rule with a transition) cannot leak
   motion to a reader who opted out. */
@media (prefers-reduced-motion: reduce) {
   .lts-scene,
   .lts-scene * {
      transition: none !important;
      animation: none !important;
      scroll-behavior: auto;
   }
}

/* --------------------------------------------------------------------------
   Inside a Visual Lab stage

   The lab supplies its own stage chrome - toolbar, zoom, fullscreen, narration -
   so the figure here is only the drawing. It drops its own frame and fills the
   landscape it replaces.

   The legacy node/edge layers are emptied by the bridge rather than merely
   covered, so nothing stale stays focusable underneath.
   -------------------------------------------------------------------------- */

.vl-motion-scene {
   margin: 0;
   border: 0;
   border-radius: 0;
   background: none;
   box-shadow: none;
   width: 100%;
   height: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
}

.vl-motion-scene .lts-scene-mount,
.vl-motion-scene .lts-scene-live {
   width: 100%;
}

/* The lab stage is a fixed-height viewport, so the scene fits inside it rather
   than driving the page height the way an in-article figure does. */
.vl-motion-scene .lts-scene-stage {
   max-height: 100%;
   margin: 0 auto;
}

/* Legacy world is emptied when a motion scene takes over; collapse it so it
   cannot contribute height to the landscape. */
.vl-landscape--motion .vl-stage__world {
   display: none;
}

/* The legacy renderer sizes and transforms the landscape itself (configureLandscape
   + applyCamera write --vl-landscape-* and a transform). The motion path skips all
   of that - a scene scales by viewBox - so the landscape must simply fill its
   viewport. Without this the figure keeps whatever size the previous legacy scene
   left behind and is clipped by the stage toolbar. */
.vl-landscape--motion {
   width: 100% !important;
   height: 100% !important;
   transform: none !important;
   display: flex;
   align-items: center;
   justify-content: center;
   padding: 0;
}

/* Nothing overflows any more, so the canvas must not invite scrolling or keyboard
   panning it can no longer perform. */
.vl-motion-active .vl-stage__canvas {
   overflow: hidden;
}

/* Pan/zoom drive the legacy camera. In motion mode they would be controls that do
   nothing, which is worse than absent. */
.vl-motion-active .vl-stage__zoom,
.vl-motion-active [data-vl-camera-toggle] {
   display: none;
}

/* ── Scene-sync: prose beats drive the figure (:::scene-sync) ─────────────────
   Reading IS the scrubber. The server bakes a plain figure + beats; the driver
   (public/js/lts-scene-sync.js) adds `is-live` when it wires up, and EVERYTHING
   sync-specific is gated on that class — with no JS the figure keeps its poster
   and caption list and the beats read as ordinary prose. */
.lts-scene-sync { position: relative; }
.lts-scene-sync .lts-beats { position: relative; z-index: 1; }
.lts-beat > :first-child { margin-top: 0; }
.lts-beat > :last-child { margin-bottom: 0; }

/* Live: pin the figure just under the site header while the beats scroll past
   beneath it. The width clamp keeps the stage to roughly the top half of the
   viewport so the active beat always has room below the fold line — and it is
   HEIGHT-derived, not a fixed pixel width: at the house 1200:660 stage ratio,
   width = height x 1.82, so capping height at ~50vh - 40px gives
   max-width ≈ 90vh - 60px. A fixed 680px cap read tiny on large displays
   (scene-internal text scales with stage width). Short viewports land near
   the old 680px; tall ones let the stage grow until the column binds. */
.lts-scene-sync.is-live .lts-scene {
   position: sticky;
   top: 68px;
   z-index: 3;
   max-width: min(100%, calc(90vh - 60px));
   margin: 0 auto 8px;
}
.lts-scene-sync.is-live .lts-beats { padding: 4vh 0 12vh; }
.lts-scene-sync.is-live .lts-beat {
   /* em-based so the measure tracks the host's type scale: ~57ch in the
      19px reader, and it grows in step with the player's presentation size. */
   max-width: 30em;
   margin: 0 auto;
   padding: 1rem 1.1rem;
   min-height: 24vh;
   display: flex;
   flex-direction: column;
   justify-content: center;
   opacity: .35;
   transition: opacity .25s ease;
}
.lts-scene-sync.is-live .lts-beat.is-active { opacity: 1; }

/* Scroll cue — the sync block's one instruction. Hidden at the no-JS floor
   (there is no sync to explain), shown when the driver goes live, and faded
   (not removed — no layout shift under the reader) once they advance a beat. */
.lts-sync-cue { display: none; }
/* Prominent by design: this is the block's ONE instruction, so it wears the
   house accent-pill idiom (orange tint + orange hairline — same combo as the
   re-explain active tab) rather than floating as fine print on the page. */
.lts-scene-sync.is-live .lts-sync-cue {
   display: flex;
   align-items: center;
   justify-content: center;
   pointer-events: none; /* decorative — must never intercept clicks, especially once faded */
   gap: 10px;
   width: fit-content;
   margin: 16px auto 0;
   padding: 10px 20px;
   background: #F3E2D2;
   border: 1px solid var(--in-orange, #E55A12);
   border-radius: 999px;
   font-family: var(--in-mono, ui-monospace, monospace);
   font-size: clamp(12px, 9px + 0.35vw, 15.5px);
   letter-spacing: .08em;
   text-transform: uppercase;
   color: var(--in-orangeD, #C24A0B);
   box-shadow: 0 2px 10px rgba(35, 35, 32, .10);
   opacity: 1;
   transition: opacity .35s ease;
}
.lts-scene-sync.is-live.is-started .lts-sync-cue { opacity: 0; }
.lts-sync-cue__ar { animation: lts-cue-bob 1.6s ease-in-out infinite; }
@keyframes lts-cue-bob {
   0%, 100% { transform: translateY(0); }
   50% { transform: translateY(4px); }
}
@media (prefers-reduced-motion: reduce) {
   .lts-scene-sync.is-live .lts-beat { transition: none; }
   .lts-sync-cue__ar { animation: none; }
}
