/* ═══════════════════════════════════════════════════════════════════════════
   presentation.css — STORY-AGNOSTIC presentation chrome for FFV bio-Stories
   ═══════════════════════════════════════════════════════════════════════════

   F10 engine foundation (staging-josephs-portrait S1, 2026-06-08). This is the
   shared presentation-engine chrome that every standalone bio-Story route
   (/stories/joseph/, /stories/charles/, /stories/laura-updegrove/, …) needs:
   the .presentation-root reset, design tokens, top nav, narrative (listen/read)
   toggle, chapter deck, the right-side control strip + scrub bar, the timed
   .visual-frame crossfade, the Bio-Story title-layering contract, the
   master-clock freeze, the aspect-true media frame, the cold-open cue-bundle
   iframe styling, the archival-clip visual type, the narration transcript / text
   panel, the chapter header + exit + header controls, the presentation container
   sizing + embedded mode + responsive, the Unified-Engine scene-stage media
   items + MediaPool doc thumbnails + Media Inspect modal.

   PROVENANCE: copied byte-for-byte from prototype/about/about.css (the route
   where this chrome originated and still lives for the About product story):
     • about.css lines   1–926   → engine shell + shared chrome through the
                                    narration text panel.
     • about.css lines 2173–3462  → chapter header → control/scrub → unified
                                    scene-stage → media items / MediaPool /
                                    inspect modal → responsive additions.
   DELIBERATELY NOT copied (About-specific cue styling — stays in about.css):
     • about.css lines  927–2172  → the inline visual-TYPE renderers (title-card,
                                    stat-grid, feature-cards, pipeline, diagram,
                                    timeline, video-stage, photo, ffa-frame, …),
                                    which a cue-bundle bio-Story never renders.
     • about.css lines 3463–end   → stat-dashboard / stat-rollup / compound and
                                    the About product's bespoke chapter cue-scenes
                                    (cold-open SVG scene, chapter-opener,
                                    constellation, knowledge-graph, kg-infographic).

   WHY: a bio-Story is data + cues + scaffold over the FROZEN engine. Its cues
   are self-styled iframes; the ONLY CSS a bio-Story borrows from the route shell
   is this engine chrome. Before F10, Joseph + Charles linked ../../about/about.css
   directly (the tempering-SR friction: chrome CSS should be a shared
   presentation.css, not a route-specific about.css). F10 FIXES that friction.

   ⚠ EXTRACTION DISCIPLINE (Iron Law): if a repointed bio-Story cue loses styling,
   ADD the missing shared rule HERE — never re-add an about.css <link> as a
   workaround. The extraction is the fix.

   Tokens are still named --about-* below (byte-preserved to avoid any cascade
   breakage); a future train may rename them --pres-*. Do not rename here.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════
   FFA Interactive Showcase — Styles
   archive.fergi.com/about/
   Layout: Fixed deck — one chapter card at a time
   ═══════════════════════════════════════════════════ */

/* ── Design Tokens ─────────────────────────────── */
:root {
    --about-primary: #667eea;
    --about-primary-end: #764ba2;
    --about-accent: #f59e0b;
    --about-success: #10b981;
    --about-success-end: #059669;
    --about-bg: #0f0f1a;
    --about-bg-section: #1a1a2e;
    --about-bg-card: #16213e;
    --about-text: #e2e8f0;
    --about-text-muted: #94a3b8;
    --about-text-bright: #ffffff;
    --about-border: rgba(255,255,255,0.08);
    --about-glow: rgba(102,126,234,0.3);
    --about-radius: 12px;
    --about-radius-sm: 8px;
    --about-radius-lg: 20px;
    --about-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
    --about-font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}

/* ── Reset & Base ──────────────────────────────── */
/* Scoped under .presentation-root so embedding in the SPA is safe */
.presentation-root, .presentation-root *, .presentation-root *::before, .presentation-root *::after {
    box-sizing: border-box; margin: 0; padding: 0;
}
.presentation-root {
    font-family: var(--about-font);
    background: var(--about-bg);
    color: var(--about-text);
    line-height: 1.6;
    overflow-x: hidden;
    overflow-y: hidden;
    -webkit-font-smoothing: antialiased;
    -webkit-text-size-adjust: 100%;
}

/* ── Modal z-index tier tokens (conducting-about-precision S1, LEDGER X4) ──
   ROOT CAUSE of "UMI opens BEHIND the stage + can't exit" (frame 0125): the
   standalone /about/ surface loads unified-media-modal.css (which sets
   `.um-overlay { z-index: var(--z-modal-umi) }`) but NOT its token provider
   modal-foundation.css — dropped in the round-8 OOM lean-standalone reduction;
   the restoring-about-mediaheart reseat re-added the modal CSS but not the
   vars. With --z-modal-umi UNDEFINED the z-index declaration is invalid and
   computes to `auto`, so the position:fixed overlay paints behind every
   positive-z stage element (deck/header/visual-frame use z 1–10). Its own ×
   was therefore unreachable, forcing Joe to exit the whole story.
   Defining the tier tokens here — the lean-standalone analogue of
   modal-foundation.css :root, values mirrored verbatim (no renumber) — restores
   UMI to 10400 (above the stage), which makes its × clickable and its
   close→PresentationPlayer.forceUnfreeze() resume path usable (paused-and-
   resumable story, not a full story-exit). Inherits to the body-appended
   .um-overlay because custom properties cascade. Scoped to .presentation-root
   so SPA embedding is unaffected. Guarded by audit_cue_timeline.js
   checkUmiZIndexContract (HARD-fail if this token block regresses). */
.presentation-root {
    --z-modal-curator: 10100;     /* person / curator overlays */
    --z-modal-umi: 10400;         /* Unified Media Modal (UMI Heart) — the X4 fix */
    --z-mobile-drawer: 12000;     /* hamburger drawer */
    --z-modal-fullscreen: 15000;  /* story takeover */
    --z-modal-ai: 20000;          /* AI fullscreen */
    --z-modal-focus: 30000;       /* focus pickers */
    --z-modal-system: 100000;     /* context modal, story-gen */
    --z-modal-critical: 100001;   /* always-top */
}

/* ── Top Navigation ────────────────────────────── */
/* mastering-about-production S1 (M-1, 2026-05-25): the top "Ferguson Family
   Vault" announcement panel is dropped story-wide. Joe directive (MasterPass
   walkthrough 22:15-22:57): "maybe we don't need the top panel announcing
   the story name. There's a top panel that says 'Ferguson Family Vault'.
   We might do without that — that gets us more vertical space. We need
   the control bar at the top, the media carousel at the bottom, organize
   the screen around that."
   The .pres-nav DOM is still built by DOMBuilder.createNav() for surface
   compatibility (other stories may use it) but hidden via display:none on
   /about/. The .pres-deck now anchors at top:0 because no nav reserves
   the 56px previously above it — the .pres-header inside the deck IS the
   control bar at the top per Joe's directive. */
.about-nav, .pres-nav {
    display: none;
}

.about-nav-logo, .pres-nav-logo {
    font-size: calc(18px * var(--type-scale, 1));
    font-weight: 700;
    background: linear-gradient(135deg, var(--about-primary), var(--about-primary-end));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    white-space: nowrap;
}

/* ── Narrative Toggle (in deck-controls, above bubble nav) ── */
.narrative-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    cursor: pointer;
    border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.15);
    background: rgba(255,255,255,0.06);
    transition: all 200ms;
    user-select: none;
    margin-bottom: 4px;
}
.narrative-toggle:hover {
    background: rgba(255,255,255,0.12);
    border-color: rgba(255,255,255,0.3);
}
.narrative-toggle input[type="checkbox"] {
    display: none;
}
.narrative-toggle-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    color: rgba(255,255,255,0.7);
    transition: color 200ms;
}
/* Voice icon shown by default (listen mode), text icon when checked (read mode) */
.narrative-toggle .nt-icon-text { display: none; }
.narrative-toggle .nt-icon-voice { display: block; }
.narrative-toggle input:checked ~ .narrative-toggle-icon .nt-icon-voice { display: none; }
.narrative-toggle input:checked ~ .narrative-toggle-icon .nt-icon-text { display: block; }
.narrative-toggle input:checked ~ .narrative-toggle-icon {
    color: var(--about-text-bright);
}

/* ── Chapter Deck (fixed card layout) ────────────── */
/* Height chain fix: inset anchoring, no percentage heights.
   mastering-about-production S1 (M-1, 2026-05-25): top changed 56px → 0
   because the .pres-nav top panel is now display:none on /about/ (Joe
   directive — drop the "Ferguson Family Vault" announcement panel).
   The .pres-header inside this deck (absolute, top:0) is now the
   top-of-window control bar. */
.chapter-deck, .pres-deck {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: hidden;
}

.chapter-card, .pres-card {
    /* SYS-5 (tightening-about-cues S1, 2026-05-23): right padding raised
       40px -> 92px to ALLOCATE a distinct vertical band for the
       .pres-controls cue-progress rail (which sits position:fixed at
       right:20px). Previously the rail visually overlapped the stage
       (Joe Camtasia bursts 6,7,9 — "the chapter navigation controls on
       the right-hand side ... should be in the distinct vertical panel"
       + "you're about bisected by the right of the stage"). The
       legacy .visual-frame `padding-right: 76px` workaround is removed
       below — the card padding now reserves the band consistently for
       ALL visual frame types (not just cue-bundle/stage-wing). The left
       padding is tightened (20px -> 16px) to reclaim the unused left
       band Joe observed.
       mastering-about-production S3 (M-14, 2026-05-25): bottom padding
       raised 70px -> 86px to clear the .pres-media-carousel overlap.
       The carousel sits position:absolute bottom:76 height:140 → its top
       edge lands at viewport y=684 (at the 900px reference height). The
       deck inset-bottom=140 (from .has-carousel rule) ends the deck at
       y=760, so the carousel overlaps the deck bottom by 76px. Pre-S3
       the card's bottom padding of 70 left the iframe content area
       extending to y=690 — 6px inside the carousel overlay zone, just
       enough to clip walk-list tails / footer chrome on Joe's MasterPass
       (06:14 'this cue got clipped on the bottom'). Bumping to 86 leaves
       a clean 10px gap (iframe content bottom at y=674; carousel top at
       y=684). Applies story-wide; cue compositions absorb the 16px
       reduction by re-centering within the slightly shorter window. */
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 48px 92px 86px 16px;
    opacity: 0;
    visibility: hidden;
    transition: opacity 350ms ease, visibility 350ms;
    overflow: hidden;
}
/* In embedded mode: chapter header is present, top padding clears it.
   Right padding still allocates the cue-rail band; left tightens further.
   mastering-about-production S3 (M-14, 2026-05-25): bottom padding raised
   70 -> 86 to mirror the standalone /about/ surface. */
.presentation-embedded .chapter-card,
.presentation-embedded .pres-card {
    padding: 48px 92px 86px 12px;
}
.chapter-card.active-chapter, .pres-card.active {
    opacity: 1;
    visibility: visible;
    z-index: 2;
}
.chapter-card.exiting, .pres-card.exiting {
    opacity: 0;
    z-index: 1;
}

/* Chapter background tints (subtle hue shifts from base) */
.chapter-card:nth-child(1), .pres-card:nth-child(1)  { background: #0f0f1a; } /* ch0 — cold open */
.chapter-card:nth-child(2), .pres-card:nth-child(2)  { background: #0f111c; } /* ch1 — blue tint */
.chapter-card:nth-child(3), .pres-card:nth-child(3)  { background: #12101a; } /* ch2 — purple tint */
.chapter-card:nth-child(4), .pres-card:nth-child(4)  { background: #0f121a; } /* ch3 — teal tint */
.chapter-card:nth-child(5), .pres-card:nth-child(5)  { background: #11101c; } /* ch4 — indigo tint */
.chapter-card:nth-child(6), .pres-card:nth-child(6)  { background: #100f1a; } /* ch5 — violet tint */
.chapter-card:nth-child(7), .pres-card:nth-child(7)  { background: #0f111b; } /* ch6 — blue-gray */
.chapter-card:nth-child(8), .pres-card:nth-child(8)  { background: #120f1a; } /* ch7 — magenta tint */
.chapter-card:nth-child(9), .pres-card:nth-child(9)  { background: #0f1219; } /* ch8 — green-teal */
.chapter-card:nth-child(10), .pres-card:nth-child(10) { background: #10101c; } /* ch9 — blue-purple */

.section-inner, .pres-card-inner {
    max-width: 100%;
    width: 100%;
    position: relative;
    max-height: 100%;
}

/* Scene-stage chapters: card stretches inner to fill entirely */
.pres-card:has(.tww-stage),
.pres-card:has(.vis-screenshot-deck),
.pres-card:has(.vis-system-reveal),
.pres-card:has(.card-deck-cascade),
.pres-card:has(.cue-bundle-iframe),
.pres-card:has(.stage-wing-container) {
    align-items: stretch;
    justify-content: flex-start;
}
.pres-card-inner:has(.tww-stage),
.pres-card-inner:has(.vis-screenshot-deck),
.pres-card-inner:has(.vis-system-reveal),
.pres-card-inner:has(.card-deck-cascade),
.pres-card-inner:has(.cue-bundle-iframe),
.pres-card-inner:has(.stage-wing-container) {
    height: 100%;
    width: 100%;
    display: flex;
    flex-direction: column;
    flex-grow: 1;
}


/* ── Right-Side Control Strip ─────────────────── */
.deck-controls, .pres-controls {
    position: fixed;
    right: 20px;
    top: 50%;
    transform: translateY(-50%);
    z-index: 90;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}

/* Prev / Next buttons */
.deck-ctrl-prev,
.deck-ctrl-next {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.15);
    background: rgba(255,255,255,0.06);
    color: rgba(255,255,255,0.7);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 200ms;
}
.deck-ctrl-prev:hover,
.deck-ctrl-next:hover {
    background: rgba(255,255,255,0.12);
    color: white;
    border-color: rgba(255,255,255,0.3);
}
.deck-ctrl-prev.disabled,
.deck-ctrl-next.disabled {
    opacity: 0.3;
    cursor: default;
    pointer-events: none;
}

/* Dot container */
.deck-dots {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 6px 0;
}

/* Dots */
.deck-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: rgba(255,255,255,0.2);
    border: none;
    cursor: pointer;
    transition: all 300ms;
    position: relative;
}
.deck-dot:hover {
    background: rgba(255,255,255,0.4);
    transform: scale(1.3);
}
.deck-dot.active {
    background: var(--about-primary);
    box-shadow: 0 0 12px var(--about-glow);
    transform: scale(1.4);
}
.deck-dot-label {
    position: absolute;
    right: 20px;
    top: 50%;
    transform: translateY(-50%);
    white-space: nowrap;
    font-size: calc(12px * var(--type-scale, 1));
    color: var(--about-text-muted);
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms;
}
.deck-dot:hover .deck-dot-label { opacity: 1; }
/* The dot is drawn 10 px but is a chapter button: its TARGET is the 24 px desktop floor (44 px on a touch tablet),
 * made by an invisible ::before so the rail looks unchanged. The gap grows so neighbouring targets meet without
 * overlapping (Greg G-136, 2026-10-21: every desktop/iPad story's chapter dots measured 10×10). Pinned by
 * tests/mobile/chapter-dot-targets.test.js. */
.deck-dots { gap: 14px; }
.deck-dot::before { content: ''; position: absolute; inset: -7px; border-radius: 50%; }
@media (pointer: coarse) and (min-width: 768px) {
    .deck-dots { gap: 34px; padding: 23px 0; }   /* the end dots' targets clear whatever sits beyond the rail */
    .deck-dot::before { inset: -17px; }
}

/* Cold open dot — amber */
.cold-open-dot {
    background: rgba(245,158,11,0.3) !important;
}
.cold-open-dot.active {
    background: var(--about-accent) !important;
    box-shadow: 0 0 12px rgba(245,158,11,0.4) !important;
}

/* Play button — now lives in scrub bar (old right-side deck position removed) */
/* Base styles for standalone mode (deck controls still get these) */
.deck-ctrl-play {
    width: 52px;
    height: 52px;
    border-radius: 50%;
    border: none;
    background: linear-gradient(135deg, var(--about-primary), var(--about-primary-end));
    color: white;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 200ms;
    box-shadow: 0 4px 20px var(--about-glow);
    position: relative;
    margin-top: 4px;
}
.deck-ctrl-play:hover {
    transform: scale(1.08);
    box-shadow: 0 6px 30px var(--about-glow);
}
.deck-ctrl-play:active { transform: scale(0.96); }

.ring-svg {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none;
}

.deck-play-icon {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Time display */
.deck-time {
    font-size: calc(11px * var(--type-scale, 1));
    color: var(--about-text-muted);
    font-variant-numeric: tabular-nums;
    margin-top: 2px;
}

/* Hide play/time in read mode */
.read-mode .deck-ctrl-play { display: none; }
.read-mode .deck-time { display: none; }
/* Hide scrub bar in read mode */
.read-mode .pres-scrub { display: none; }

/* ── Visual Frames (the core timed visuals) ────── */
.visual-frame {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transform: translateY(12px);
    transition: opacity var(--about-transition), transform var(--about-transition);
    pointer-events: none;
}
.visual-frame.active {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
    position: relative;
}
/* Visual types with absolutely-positioned children need flex:1 to fill chapter pane.
   Without this, position:relative on .active causes 0px height collapse.
   Currently: screenshot-deck, card-deck-cascade, cue-bundle (iframe-mounted CD bundles),
   stage-wing (Stage + Wing dual-iframe layout).
   Add new abs-positioned types here. */
.visual-frame.active:has(.vis-screenshot-deck),
.visual-frame.active:has(.card-deck-cascade),
.visual-frame.active:has(.cue-bundle-iframe),
.visual-frame.active:has(.stage-wing-container),
.visual-frame.active:has(.vis-archival-clip) {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

/* ── Ch4 graceful cue handoff (perfecting-outstanding-about S5, LEDGER 20:15) ──
   Joe: "Cues terminate suddenly. This chapter needs to be paced slower." The
   base .visual-frame exit fade is 400ms (felt as a hard cut at cue boundaries).
   SCOPED to Ch4 only (data-cue-id^="about_ch4_") so the praised chapters' cue
   transitions are untouched: the OUTGOING cue lingers + dissolves over 720ms
   (a gentle handoff) while the INCOMING cue keeps the crisp 400ms enter (the
   base .visual-frame rule governs the fade-OUT; the scoped .active rule below
   restores the praised on-time fade-IN). Pairs with the S5 re-pace (more
   absorption time between cues). Ear/eye-gated: Joe confirms the feel at SR. */
.visual-frame[data-cue-id^="about_ch4_"] {
    transition: opacity 720ms cubic-bezier(0.4, 0, 0.2, 1),
                transform 560ms cubic-bezier(0.4, 0, 0.2, 1);
}
.visual-frame[data-cue-id^="about_ch4_"].active {
    transition: opacity var(--about-transition), transform var(--about-transition);
}

/* ── Bio-Story title-layering contract (adopting-gemini-ilm S1, A3) ──────────
   Joe U022/U025: "you don't step on existing titles on the screen when you
   introduce new ones" / "graphics stepping around each other. That's not a
   scrub artifact. It's a problem." Root cause: every bio-Story cue is a
   FULL-STAGE cue-bundle carrying its own large title. The base .visual-frame
   crossfade leaves the OUTGOING frame (position:absolute, inset:0) painting its
   title at opacity>0 while the INCOMING frame fades in — so two full-stage
   titles co-exist mid-transition (visible, persistent at slow bio pacing —
   not a scrub artifact). The CONTRACT: the active cue owns the title layer.
   It sits ABOVE any outgoing frame (z-index) on its OWN opaque stage backdrop
   (::before), so an incoming title is never undercut by the outgoing one; the
   outgoing frame's fade-out is shortened so it clears fast. Scoped to
   .story-bio (segment-nav bio-Stories) — the About showcase crossfade is
   untouched. Engineer-owned half of the title-layering contract; the
   Screenwriter/VFX half (one title region per cue) lands in S2 cue authoring. */
.story-bio .visual-frame {
    z-index: 1;
    transition: opacity 220ms cubic-bezier(0.4, 0, 0.2, 1),
                transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.story-bio .visual-frame.active {
    z-index: 2;
    transition: opacity var(--about-transition), transform var(--about-transition);
}
/* Opaque stage backdrop behind the active cue's content — occludes the
   outgoing frame's title the instant the incoming frame gains opacity. */
.story-bio .visual-frame.active::before {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--about-bg);
    z-index: -1;
    pointer-events: none;
}

/* ── X4(c) master-clock freeze (perfecting-about-symphony S1, LEDGER X4) ──────
   Joe's diagnostic (40:17): "when the timeline is PAUSED, the narration text +
   audio pause but the cue animation keeps developing." Root cause: pausing the
   transport stops the audio + the rAF that drives cue:progress + visual.update(),
   but it does NOT stop CSS @keyframes animations — those run off the wall clock,
   so any cue that animates via keyframes keeps developing while paused/frozen.
   PresentationPlayer sets data-paused="1" on the .presentation-root for BOTH
   transport-pause (_pauseAllMedia) and UMI-inspect freeze (_freezeForModal), and
   clears it on resume + _unfreezeFromModal. Pausing animation-play-state across
   the active cue subtree freezes ALL keyframe motion the instant the transport
   halts, so pause freezes audio + narration + cue together — one master clock.
   Purely additive: no effect while playing (the attribute is absent); animations
   resume from their frozen position on unpause. The iframe-bundle half (CSS
   inside cue-bundle iframes is unreachable by a parent rule) is handled by
   PresentationPlayer._setIframeAnimationFreeze, which injects the same
   animation-play-state:paused into same-origin bundle documents. Transitions are
   unaffected (only @keyframes pause) so frame enter/exit still settles. */
.presentation-root[data-paused="1"] .visual-frame.active,
.presentation-root[data-paused="1"] .visual-frame.active *,
.presentation-root[data-paused="1"] .visual-frame.active *::before,
.presentation-root[data-paused="1"] .visual-frame.active *::after,
.presentation-root[data-paused="1"] .media-item[data-slot]:not([data-slot="offstage"]),
.presentation-root[data-paused="1"] .media-item[data-slot]:not([data-slot="offstage"]) * {
    animation-play-state: paused !important;
}

/* ── X1 media-highlight default (perfecting-about-symphony S1, LEDGER X1) ──────
   CueWordSync.highlightOnWord(phrase, target) adds `ws-highlight` to a media
   thumbnail exactly as the narrator pronounces its name (one-at-a-time within a
   group). This is a gentle DEFAULT for standalone-visual consumers; a cue bundle
   may pass its own class via opts.cls and define it in the bundle's own <style>
   (the helper runs inside the bundle iframe, so a parent class will not reach it). */
.presentation-root .ws-highlight {
    outline: 3px solid var(--about-accent, #e8a33d);
    outline-offset: 2px;
    box-shadow: 0 0 0 6px rgba(232, 163, 61, 0.22);
    border-radius: 4px;
    transition: outline-color 180ms ease, box-shadow 180ms ease;
}

/* ── X1 transient flash-on-word default (conducting-about-precision S1) ─────────
   CueWordSync.flashOnWord(phrase, target) adds `ws-flash` to a target exactly as
   the narrator pronounces the word, then removes it after ~1.2s — Joe's "big
   orange border, flash it to draw attention" (e.g. the whole carousel frame on
   the word "carousel"). A default for standalone-visual consumers; a cue bundle
   passes its own opts.cls + defines the flash in its own <style> (the helper runs
   inside the iframe so a parent class won't reach it). */
.presentation-root .ws-flash {
    outline: 4px solid var(--about-accent, #e8a33d);
    outline-offset: 3px;
    border-radius: 6px;
    animation: ws-flash-pulse 0.5s ease-in-out 2;
}
@keyframes ws-flash-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(232, 163, 61, 0.0); }
    50%      { box-shadow: 0 0 0 10px rgba(232, 163, 61, 0.45); }
}

/* ── X2 true-aspect-ratio contract (perfecting-about-symphony S1, LEDGER X2) ───
   "Match the document's true ratio, fully visible, never squashed or clipped …
   correct everywhere." The aspect-true.js helper sizes a thumbnail's frame to the
   image's natural ratio + sets object-fit:contain at runtime; this CSS is the
   static contract for authors who prefer a class. Put .ffa-aspect-contain on any
   document/photo thumbnail that must show the WHOLE artifact. NEVER object-fit:
   cover on a document — it clips top/bottom (the X2 antipattern chapter_quality_
   check.py flags). */
.presentation-root .ffa-aspect-contain,
.presentation-root img[data-true-aspect] {
    object-fit: contain !important;
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
}

/* ── X2 aspect-by-media-TYPE frame (conducting-about-precision S1, LEDGER X2) ───
   The canonical media-frame: declare data-media-type on the frame and the frame
   takes its TYPE's aspect — documents 8.5×11 letter portrait (whole Citizen USA
   cover / journal / thesis visible), videos 16:9; photos + people keep their OWN
   natural ratio (no fixed frame — aspect-true.js sizes them from the image).
   Joe (22:30): "you're using the same video rectangular frame for all the
   documents … Documents are 8.5×11 … nail this down; let's not have this problem
   again." This CSS is the no-JS contract; aspect-true.js is the runtime enhancer.
   The inner media is contained so the WHOLE artifact shows, letterboxed if its
   own ratio differs. NEVER object-fit:cover on a document (clips top/bottom — the
   X2 antipattern chapter_quality_check.py flags). */
.presentation-root .ffa-media-frame {
    display: block;
    overflow: hidden;
}
.presentation-root .ffa-media-frame[data-media-type="document"],
.presentation-root .ffa-media-frame[data-media-type="doc"] {
    aspect-ratio: 8.5 / 11;   /* US letter portrait */
}
.presentation-root .ffa-media-frame[data-media-type="video"] {
    aspect-ratio: 16 / 9;
}
.presentation-root .ffa-media-frame > img,
.presentation-root .ffa-media-frame > video {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* SYS-5 (tightening-about-cues S1, 2026-05-23): the per-visual-frame
   padding-right: 76px workaround that protected ONLY cue-bundle/stage-wing
   frames from bleeding under the .pres-controls right strip is RETIRED.
   The .pres-card now reserves the rail band uniformly (padding-right: 92px),
   so every visual frame type — legacy chapter cards, scene stages, system
   reveals, screenshot decks, card-deck cascades, AND cue-bundle/stage-wing —
   gets the same protection without per-type per-frame padding tweaks.
   The rule is intentionally left empty (rather than deleted) so SR can
   re-introduce per-type tweaks here if a specific cue type ever requires
   different right-band reservation than the card baseline. */
.visual-frame.active:has(.cue-bundle-iframe),
.visual-frame.active:has(.stage-wing-container) {
    padding-right: 0;
}

/* Read mode: show all visuals stacked within the current card */
.read-mode .visual-frame {
    opacity: 1;
    transform: none;
    position: relative;
    pointer-events: auto;
    margin-bottom: 48px;
}
.read-mode .section-inner, .read-mode .pres-card-inner {
    display: flex;
    flex-direction: column;
    min-height: auto;
}
.read-mode .chapter-card, .read-mode .pres-card {
    align-items: flex-start;
    padding-top: 48px;
}

/* ── Cold Open (Chapter 0 trailer) ────────────── */
.cold-open-card .section-inner, .cold-open-card .pres-card-inner {
    max-width: 700px;
}
.cold-open-card .pres-card-inner:has(.tww-stage) {
    max-width: 100%;
}
/* F2 fix (nail-storytelling-pipeline S5, 2026-05-22): cue-bundle iframes
 * must fill the full App Panel width, not the 700px center-column default.
 * Joe Camtasia walkthrough cue 01a [3.6–11.6]: "this is the old left half,
 * so we don't have the form factor right here at all". The cold-open
 * 700px constraint dates from the legacy read-mode chapter-card layout
 * (before cue-bundle ingestion); it's the wrong default for iframe-mounted
 * production cues. Override matches the existing :has(.tww-stage)
 * override pattern but applies to cue-bundle iframes. */
.cold-open-card .pres-card-inner:has(.cue-bundle-iframe),
.cold-open-card .section-inner:has(.cue-bundle-iframe),
.cold-open-card .pres-card-inner:has(.vis-archival-clip),
.cold-open-card .section-inner:has(.vis-archival-clip) {
    max-width: 100%;
}

/* ── Archival Clip visual type (equipping-atelier-stagecraft S2) ─────────
 * First-class engine-managed archival video. Replaces the muted-cue-bundle
 * + phantom-ClipPlayer pattern (cold-open cues 05/08/10). The video is
 * unmuted by AudioDirector.enterClipMode() during the cue window; the
 * pre-baked silent gap in 00_cold_open.mp3 is filled by the clip's own
 * audio.
 *
 * Bug B fix: source clips are 540×360 (3:2). Old cue-bundles forced 4/3
 * inside an overflow:hidden iframe → top/bottom clip. New layout uses
 * the source aspect (ac-stage style="aspect-ratio:3/2") with NO overflow
 * clip in any ancestor.
 */
.vis-archival-clip {
    flex: 1;
    min-height: 0;
    width: 100%;
    display: grid;
    /* regrounding-about-presentation S2: a 2-column grid of portrait (8.5×11)
       supporting docs beside the video — the Jane Sato / Citizen USA cue
       (co_demo_06) layout Joe points to as correct (LEDGER U004/U005/U008).
       perfecting-overture-triptych S2 (D2/D7, walkthrough 2026-05-31): the wing
       is narrowed 38%→34% and the video uncapped (below) because Joe now wants
       the archival video "quite a bit bigger … as large as possible in that
       half — room is being wasted dividing the screen in half." The video-left
       + 4-doc-wing TEMPLATE SHAPE is preserved (Joe LOCK); only the video grows. */
    /* advancing-about-movements S2 (D5 / LEDGER co 04:34-05:04 frame 0280 +
       06:06-07:34 frame 0460): reclaim the "awkward vacant area between the
       left (video) and right (docs)." The wing widens 34%->38% (its left edge
       creeps toward the video, shrinking the central void AND making the 4
       supporting docs bigger per X3), and column-gap tightens 18px->14px. The
       video itself does NOT shrink (perfecting-overture-triptych "as large as
       possible" LOCK preserved); the gap is reclaimed by centering the stage
       (justify-self:center below) + the wider wing, not by growing the video. */
    grid-template-columns: minmax(0, 1fr) clamp(280px, 38%, 440px);
    grid-template-rows: 1fr auto auto;
    grid-template-areas:
        "stage wing"
        "caption wing"
        "context wing";
    column-gap: 14px;
    row-gap: 8px;
    padding: 16px 20px;
    color: var(--about-text, #e0e0e8);
    font-family: 'Source Sans 3', -apple-system, BlinkMacSystemFont, sans-serif;
}
.vis-archival-clip:not(:has(.ac-wing)) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
        "stage"
        "caption"
        "context";
}
.vis-archival-clip .ac-stage {
    grid-area: stage;
    position: relative;
    /* perfecting-overture-triptych S2 (D2/D7, walkthrough 2026-05-31): Joe —
       "make the video bigger on the left … as large as possible in that half …
       room is being wasted dividing the screen in half." The prior 620px cap +
       width:100% left a small letterboxed video centered in a tall cell. Now the
       stage fills its cell HEIGHT at the cue's true aspect (inline aspect-ratio
       from archival-clip.js render = 3/2 for the cold-open clips), clamped to the
       column width — i.e. the largest aspect-correct box that fits its half. The
       <video> object-fit:contain still guards against any residual letterbox on
       extreme cell ratios. Template-level → applies to every archival-clip cue
       (co_demo_05 Manzanar / 08 OD / 10 Corporations) at once. */
    height: 100%;
    width: auto;
    max-width: 100%;
    max-height: 100%;
    min-height: 0;
    margin: 0;
    /* advancing-about-movements S2 (D5): CENTER the aspect-correct video in its
       column (was justify-self:start, which pushed the whole void to the right
       of the video as the one-sided "awkward vacant area" Joe flagged at frame
       0280). Centered, any residual letterbox space is symmetric padding that
       reads as intentional composition rather than a one-sided gap. Joe's
       directive offered exactly this: "either center in the left or use the
       middle block." */
    justify-self: center;
    align-self: center;
    background: #0a2e2e;
    border: 1px solid rgba(224, 224, 232, 0.08);
    border-radius: 8px;
    /* NO overflow:hidden — true 3:2 stays uncliped. */
    box-shadow: 0 14px 36px rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.4);
    cursor: pointer;
}
.vis-archival-clip .ac-video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    border-radius: 8px;
    background: #082626;
}
/* S4 fixing-about-again (2026-05-27, B7): watermark overlay over the archival
   <video>. Joe directive U034 — "would be nice to have [Archival voice · cassette
   · 1987 · Manzanar 1942] superimposed on the screen as a watermark." Opt-in via
   the cue's visual.content.watermark field; CSS overlay only, no source re-encode. */
.vis-archival-clip .ac-stage { position: relative; }
.vis-archival-clip .ac-watermark {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    padding: 8px 14px;
    background: linear-gradient(to top, rgba(8, 38, 38, 0.55), rgba(8, 38, 38, 0));
    color: rgba(255, 255, 255, 0.78);
    font-size: calc(11px * var(--type-scale, 1));
    letter-spacing: 0.10em;
    text-transform: uppercase;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
    pointer-events: none;
    font-style: italic;
    border-bottom-left-radius: 8px;
    border-bottom-right-radius: 8px;
}
.vis-archival-clip .ac-caption {
    grid-area: caption;
    font-size: calc(13px * var(--type-scale, 1));
    line-height: 1.4;
    color: #dadde0;
}
.vis-archival-clip .ac-context {
    grid-area: context;
    font-size: calc(11px * var(--type-scale, 1));
    letter-spacing: 0.06em;
    color: #6c7680;
    text-transform: uppercase;
}
/* regrounding-about-presentation S2 (LEDGER U002/U003/U005/U008 — RECURRING,
   reported 3–4×): supporting docs in the archival-clip wing must be 8.5×11
   PORTRAIT, ~3–4 of them, matching the co_demo_06 Citizen USA grid — NOT the
   landscape thumbnails that kept regressing. Fixed at the template/stage-layout
   level (this CSS + the render-time guard in archival-clip.js), so no per-cue
   bundle can reintroduce the landscape form factor. */
.vis-archival-clip .ac-wing {
    grid-area: wing;
    display: grid;
    grid-template-columns: 1fr 1fr;
    /* perfecting-overture-triptych S2 (D3/X6): the wing's bottom row clipped
       (Citizen USA / Manzanar Journal cut off at the panel bottom — frame
       0150/0210) because auto-rows + align-content:start piled the tall
       portrait docs past the wing bottom with overflow:auto (no scroll in a
       presentation → clip). Rows now SHARE the wing height equally so every
       row fits; the bottom row is no longer clipped. Same no-clip discipline
       co_demo_06's LOCKED-good 4-doc grid uses. */
    grid-auto-rows: minmax(0, 1fr);
    gap: 12px;
    align-content: stretch;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
}
/* LS4-10 (Joe /listen cluster 4, 2026-08-14): "present a SINGLE document thumbnail
   below the video and that should be the Manzanar Journal ... for the first archival
   video." A one-item wing left in the 2x2 grid would seat one small card in the
   top-left quarter of a 440px column and leave three empty cells — a smaller
   presentation than the four it replaced, which is the opposite of the ask. When the
   wing holds exactly one card it collapses to a single full-width, full-height cell,
   so the one document Joe named reads LARGE. Content-driven, so no cue can get this
   wrong: the rule keys off how many cards the manifest actually supplies.
   `object-fit: contain` on the img already keeps the whole page visible. */
.vis-archival-clip .ac-wing:has(> .ac-wing-card:only-child) {
    grid-template-columns: 1fr;
}
.vis-archival-clip .ac-wing-card {
    position: relative;
    padding: 6px;
    background: rgba(15, 54, 54, 0.55);
    border: 1px solid rgba(224, 224, 232, 0.08);
    border-radius: 6px;
    cursor: pointer;
    transition: border-color 400ms ease, background 400ms ease, opacity 400ms ease;
    opacity: 0.55;
    display: flex;
    flex-direction: column;
    gap: 6px;
    /* S2 (D3/X6): allow the card to shrink within its (now height-shared) row
       so the img + caption fit without pushing the bottom row off-panel. */
    min-height: 0;
    overflow: hidden;
}
.vis-archival-clip .ac-wing-card img {
    display: block;
    width: 100%;
    /* perfecting-overture-triptych S2 (D3/X6): full document, no crop, no
       bottom-clip. The img flex-shrinks to its share of the card height (the
       wing rows now share the wing height equally); object-fit:contain keeps
       the whole portrait page visible and the paper tone backs any letterbox.
       Replaces the prior aspect-ratio:8.5/11 + max-height:none, which forced
       the img taller than its row and clipped the bottom row of docs. */
    flex: 1 1 0;
    min-height: 0;
    height: auto;
    max-height: 100%;
    object-fit: contain;
    background: #f4ede0;
    border-radius: 3px;
}
.vis-archival-clip .ac-wing-card:hover {
    border-color: rgba(58, 128, 144, 0.5);
    background: rgba(58, 128, 144, 0.10);
}
.vis-archival-clip .ac-wing-card.ac-wing-card--active {
    opacity: 1;
    border-color: #e8a040;
    background: rgba(232, 160, 64, 0.08);
    box-shadow: 0 0 0 1px rgba(232, 160, 64, 0.20), 0 6px 16px rgba(0, 0, 0, 0.3);
}
.vis-archival-clip .ac-wing-cap {
    font-size: calc(11px * var(--type-scale, 1));
    line-height: 1.32;
    color: #a8b0b8;
}
.vis-archival-clip .ac-wing-card.ac-wing-card--active .ac-wing-cap {
    color: #e8a040;
}

/* perfecting-overture-triptych S2 (D8): graceful end-of-clip settle. When an
   archival clip's video ends before its cue window closes, the frozen last
   frame read as "awkward dead air" (Joe LEDGER co 12:08–13:02). On the video's
   'ended', archival-clip.js adds .archival-clip-ended to the cue FRAME (an
   ancestor of .ac-stage / .ac-wing); the frozen stage RECEDES + the supporting
   docs rise to full presence, so the cue tail becomes a reflective
   career-documents beat rather than a dead frozen face.
   vaulting-paired-storycraft S1 (A3): the ROOT cause of Joe's "video's frozen,
   transcript scrolling over it" was the held-frozen TAIL — the Manzanar clip
   was held ~7.6s because the next cue's phraseKey resolved mid-sentence (fixed
   in the manifest; held tails now ~1s for all 3 clips). This settle is the
   visual finish: at opacity 0.5 the frozen FACE was still half-present (Joe
   "the video's frozen"); recede it firmly (≈0.22 + desaturate + slight scale)
   so the residual ~1s tail reads as documents, not a frozen face. Eye-gated to
   Joe's next Camtasia (§215). */
.archival-clip-ended .ac-stage {
    opacity: 0.22;
    filter: saturate(0.55) brightness(0.62);
    transform: scale(0.985);
    transition: opacity 600ms cubic-bezier(0.4, 0, 0.2, 1),
                filter 600ms cubic-bezier(0.4, 0, 0.2, 1),
                transform 600ms cubic-bezier(0.4, 0, 0.2, 1);
}
.archival-clip-ended .ac-wing-card {
    opacity: 1;
    transition: opacity 1100ms cubic-bezier(0.4, 0, 0.2, 1), border-color 400ms ease;
}

.cold-open-card .narration-transcript {
    font-size: calc(1.1rem * var(--type-scale, 1));
    border-left-color: var(--about-accent);
    background: rgba(245,158,11,0.06);
}

/* ── Narration Transcript (Read mode) ──────────── */
.narration-transcript {
    display: none;
    background: rgba(102,126,234,0.06);
    border-left: 3px solid var(--about-primary);
    border-radius: 0 var(--about-radius-sm) var(--about-radius-sm) 0;
    padding: 20px 24px;
    margin-bottom: 32px;
    font-size: calc(1rem * var(--type-scale, 1));
    line-height: 1.8;
    color: var(--about-text);
}
.read-mode .narration-transcript { display: block; }

/* ── Narration Text Panel (above scrub bar at bottom of presentation) ── */
.narration-text-panel, .pres-subtitles {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 60px;
    text-align: center;
    padding: 8px 24px;
    background: rgba(15,15,26,0.92);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border-top: 1px solid var(--about-border);
    font-size: calc(0.9rem * var(--type-scale, 1));
    color: var(--about-text-bright);
    line-height: 1.5;
    /* converging-quartet-storyflow S2 (A9, the GENERAL RULE): never leave a single dangling word
       on a subtitle's last line (Joe's "afternoon" orphan). text-wrap:pretty is the browser-native
       last-line-orphan avoider; here on the engine-global subtitle bar so every Story (Laura/David/
       Andy + future bios) inherits it, not just About. Progressive enhancement — older engines ignore it. */
    text-wrap: pretty;
    z-index: 80;
    min-height: 52px; /* Always reserve space for ~2 lines */
    max-height: 76px; /* 2 lines of text with padding */
    overflow-y: auto;
    opacity: 1;
    pointer-events: auto;
}
/* Dim slightly when no active sentence (between lines) */
.narration-text-panel.between-lines, .pres-subtitles.between-lines {
    opacity: 0.6;
}
.narration-text-panel:empty, .pres-subtitles:empty {
    opacity: 0.4;
}
/* Read mode: narration text panel stays visible for scrolling narrative text */


/* ───────────── (about.css 2173–3462: chapter header → controls → scene-stage → media → responsive) ───────────── */

/* ── Chapter Header (story name + chapter title + Comment) ── */
.chapter-header, .pres-header {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 16px;
    padding-right: 16px;
    /* G-019 (Greg, 2026-09-28): in the iPad Home Screen app this header sits at the physical top of
       the screen, under the status bar: env(safe-area-inset-top) measured 32px while the title and
       the narration/speed/lock/Play row occupied y 16-44, the title unreadable behind the clock.
       The inset goes on the header (the presentation-toolkit.css .fullscreen-header precedent);
       it is 0 on every desktop, so nothing changes there. */
    padding-top: calc(8px + env(safe-area-inset-top, 0px));
    background: rgba(15,15,26,0.8);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--about-border);
}

.chapter-header-title, .pres-header-title {
    font-size: calc(0.85rem * var(--type-scale, 1));
    color: var(--about-text-muted);
    flex-shrink: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* mastering-about-production S1 (M-1, 2026-05-25): the "Ferguson Family
   Vault — " preamble inside the .pres-header title is dropped story-wide
   per Joe's "drop the top FFV panel" directive (the visible chapter name
   remains as a minimal context label for the control bar — useful and
   non-redundant with the chapter-dot strip). DOM stays intact for any
   surface that needs the full title; CSS hides only on /about/. */
.pres-header-story {
    display: none;
}

.pres-header-separator {
    display: none;
}

.chapter-header-name, .pres-header-name {
    color: var(--about-text-bright);
    font-weight: 600;
}

/* ── Universal Exit Button ── */
/* 44px touch target (iOS minimum), subtle visual */
.ffa-exit-btn {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.15);
    background: rgba(255,255,255,0.05);
    color: var(--about-text-muted);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: all 200ms;
    padding: 0;
}
.ffa-exit-btn:hover {
    background: rgba(255,255,255,0.12);
    border-color: rgba(255,255,255,0.4);
    color: var(--about-text-bright);
}
.pres-header .pres-header-narration {
    margin-left: auto;
}

/* ── Header Controls: Narration ── */
.pres-header-narration {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.2);
    background: rgba(255,255,255,0.05);
    color: var(--about-text-muted);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: all 200ms;
    padding: 0;
}
.pres-header-narration:hover {
    background: rgba(255,255,255,0.12);
    border-color: rgba(255,255,255,0.4);
    color: var(--about-text-bright);
}

/* Narration on/off icon toggle */
.pres-header-narration .pres-icon-narr-off { display: none; }
.pres-header-narration.narration-off .pres-icon-narr-on { display: none; }
.pres-header-narration.narration-off .pres-icon-narr-off { display: flex; }
.pres-header-narration.narration-off {
    opacity: 0.5;
    border-style: dashed;
}

/* Playback-speed pill (conducting-about-precision S1, LEDGER X9). Text control
   ("1×"/"1.2×"/"1.5×") sitting between the narrate + lock buttons; matches their
   visual language (height, border, hover) but auto-width with tabular digits so
   the label stays steady as it cycles. */
.pres-header-speed {
    height: 28px;
    min-width: 36px;
    padding: 0 9px;
    border-radius: 14px;
    border: 1px solid rgba(255,255,255,0.2);
    background: rgba(255,255,255,0.05);
    color: var(--about-text-muted);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: calc(12px * var(--type-scale, 1));
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
    transition: all 200ms;
}
.pres-header-speed:hover {
    background: rgba(255,255,255,0.12);
    border-color: rgba(255,255,255,0.4);
    color: var(--about-text-bright);
}
.pres-header-speed.speed-boosted {
    color: var(--about-accent);
    border-color: var(--about-accent);
    background: rgba(245,158,11,0.1);
}

/* Scrub bar when inside the header */
.pres-header .pres-scrub {
    position: static;
    flex: 1;
    min-width: 0;
    background: transparent;
    border-top: none;
    padding: 0;
    z-index: auto;
}

/* ═══════════════════════════════════════════════════
   Embedded Mode — Presentation inside the SPA App Panel
   Applied when .presentation-embedded is on the container.
   ═══════════════════════════════════════════════════ */

/* About view: prevent any overflow from presentation filling the App Panel */
#view-about {
    overflow: hidden;
}

.presentation-embedded {
    position: relative;
    overflow: hidden;
    /* When inside SPA .main-content (which has padding), negate it to fill App Panel exactly */
    margin: calc(-1 * var(--space-xl, 2rem));
    width: calc(100% + 2 * var(--space-xl, 2rem));
    height: calc(100vh - var(--nav-height, 60px));
    height: calc(100dvh - var(--nav-height, 60px)); /* dvh: accurate on mobile Safari */
    padding: 0;
}

/* Hide presentation nav in embedded mode — SPA has its own header */
.presentation-embedded .about-nav, .presentation-embedded .pres-nav {
    display: none;
}

/* Deck fills entire embedded container (no pres-nav offset) */
.presentation-embedded .chapter-deck, .presentation-embedded .pres-deck {
    top: 0;
    bottom: 0;
}

.presentation-embedded .deck-controls, .presentation-embedded .pres-controls {
    position: absolute;
    z-index: 40;
}

.presentation-embedded .narration-text-panel, .presentation-embedded .pres-subtitles {
    position: absolute;
    z-index: 45;
}

/* ── Responsive ────────────────────────────────── */
@media (max-width: 768px) {
    .about-nav, .pres-nav {
        padding: 0 16px;
        height: 48px;
    }
    .about-nav-logo, .pres-nav-logo { font-size: calc(15px * var(--type-scale, 1)); }

    .chapter-deck, .pres-deck {
        top: 48px;
    }

    /* Embedded mode: no pres-nav, deck starts at top */
    .presentation-embedded .pres-deck {
        top: 0;
    }

    .chapter-card, .pres-card {
        padding: 44px 56px 64px 12px;
    }

    .presentation-embedded .pres-card {
        padding: 44px 48px 56px 8px;
    }

    .chapter-header, .pres-header {
        padding: 6px 10px;
        padding-right: 52px;
        gap: 8px;
    }
    .chapter-header-title, .pres-header-title { font-size: calc(0.78rem * var(--type-scale, 1)); }
    .pres-header-narration, .pres-header .ffa-exit-btn { width: 24px; height: 24px; }
    .pres-header-narration svg, .pres-header .ffa-exit-btn svg { width: 12px; height: 12px; }

    .narrative-toggle {
        width: 30px;
        height: 30px;
    }

    /* Smaller controls on mobile */
    .deck-controls, .pres-controls {
        right: 12px;
        gap: 6px;
    }
    .deck-ctrl-prev,
    .deck-ctrl-next {
        width: 28px;
        height: 28px;
    }
    .deck-dot {
        width: 8px;
        height: 8px;
    }
    .deck-dots {
        gap: 8px;
    }
    .deck-dot::before { inset: -4px; }   /* 8 px dots 8 px apart: targets meet, never overlap (G-136) */
    .deck-ctrl-play {
        width: 44px;
        height: 44px;
    }
    .ring-svg {
        width: 44px;
        height: 44px;
    }

    .screenshot-frame.phone {
        width: 220px;
        height: 390px;
    }
    .screenshot-frame.desktop {
        height: 240px;
    }

    .vis-feature-cards {
        grid-template-columns: 1fr;
    }

    .vis-stat-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .pipeline-track {
        flex-wrap: wrap;
        gap: 8px;
        justify-content: center;
    }
    .pipeline-arrow { display: none; }
    .pipeline-node {
        flex: 0 0 auto;
    }

    .arch-layer {
        flex-direction: column;
        align-items: flex-start;
        gap: 4px;
    }
    .arch-layer-detail { margin-left: 0; }

    .story-card {
        flex-direction: column;
        text-align: center;
    }

    .vis-diagram.connection-flow {
        flex-direction: column;
    }
    .diagram-flow-arrow {
        transform: rotate(90deg);
    }

    /* Video Stage responsive */
    .vis-video-stage.layout-focus {
        min-height: 40vh;
        padding: 12px;
        gap: 8px;
    }

    .vis-video-stage.layout-focus .stage-primary {
        max-height: 45vh;
    }

    .vis-video-stage.layout-focus .stage-secondary {
        max-height: 18vh;
    }

    .vis-video-stage.layout-focus .stage-tertiary {
        max-height: 15vh;
    }

    .vis-video-stage.layout-triptych {
        gap: 6px;
        padding: 12px;
    }

    .vis-video-stage.layout-triptych .stage-slot {
        min-height: 30vh;
    }

    .stage-caption {
        font-size: calc(0.7rem * var(--type-scale, 1));
        padding: 8px 10px;
    }

    .stage-title-text {
        font-size: clamp(1.2rem, 4vw, 1.8rem);
    }
}

@media (max-width: 480px) {
    .vis-stat-grid {
        grid-template-columns: 1fr;
    }
    .vis-title-card { padding: 40px 10px; }

    .vis-video-stage.layout-focus .stage-secondary {
        opacity: 0.6;
    }

    .vis-video-stage.layout-focus .stage-tertiary {
        display: none;
    }

    .vis-video-stage.layout-triptych .stage-slot {
        min-height: 22vh;
    }
}

/* ── Loading State ─────────────────────────────── */
.about-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    font-size: calc(1.2rem * var(--type-scale, 1));
    color: var(--about-text-muted);
}
.about-loading .spinner {
    width: 32px;
    height: 32px;
    border: 3px solid var(--about-border);
    border-top-color: var(--about-primary);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    margin-right: 12px;
}
@keyframes spin {
    to { transform: rotate(360deg); }
}

/* ═══════════════════════════════════════════════════
   Phase 2: StageManager — Cast-Based Visual Slots
   ═══════════════════════════════════════════════════ */

/* Stage container — CSS Grid for primary/sub slots */
.stage-manager {
    display: grid;
    grid-template-columns: 1fr 3fr 1fr;
    grid-template-rows: 3fr 1fr;
    gap: 12px;
    width: 100%;
    height: 100%;
    min-height: 50vh;
    padding: 20px;
    position: relative;
}

/* Full-width for stage chapters */
.section-inner:has(.stage-manager) {
    max-width: 100%;
}

/* Base stage item */
.stage-item {
    border-radius: var(--about-radius);
    overflow: hidden;
    background: var(--about-bg-card);
    transition: opacity 400ms ease, transform 400ms ease,
                grid-column 400ms ease, grid-row 400ms ease,
                max-height 400ms ease;
    cursor: pointer;
    position: relative;
}
.stage-item:not(.placeholder):hover {
    filter: brightness(1.1);
    transform: scale(1.02);
}
.stage-item img,
.stage-item video {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

/* Placeholder items — must have explicit dimensions since no intrinsic media size */
.stage-item.placeholder {
    cursor: default;
    border: 2px dashed rgba(102,126,234,0.35);
    background: rgba(102,126,234,0.08);
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 80px;
}
.stage-item.placeholder.slot-primary {
    min-height: 200px;
    align-self: stretch;
}
.stage-item.placeholder.slot-sub-left,
.stage-item.placeholder.slot-sub-right {
    min-height: 100px;
    align-self: stretch;
}
.stage-placeholder-label {
    font-size: calc(0.9rem * var(--type-scale, 1));
    color: rgba(255,255,255,0.6);
    letter-spacing: 0.02em;
}

/* ── Slot Classes (Grid-based positioning) ── */

.slot-primary {
    grid-column: 1 / -1;
    grid-row: 1;
    max-height: 65vh;
    z-index: 3;
    align-self: center;
    box-shadow: 0 8px 40px rgba(0,0,0,0.5);
}

.slot-sub-left {
    grid-column: 1;
    grid-row: 2;
    max-height: 25vh;
    z-index: 2;
    align-self: start;
    opacity: 0.85;
    box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}

.slot-sub-right {
    grid-column: 3;
    grid-row: 2;
    max-height: 25vh;
    z-index: 2;
    align-self: start;
    opacity: 0.85;
    box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}

/* Decoration items — absolute positioned around the perimeter */
.slot-deco {
    position: absolute;
    width: 120px;
    height: 80px;
    opacity: 0.6;
    z-index: 1;
    border-radius: var(--about-radius-sm);
    box-shadow: 0 2px 12px rgba(0,0,0,0.3);
    transition: opacity 400ms ease, transform 400ms ease;
}
.slot-deco.pos-top-left     { top: 24px; left: 24px; }
.slot-deco.pos-top-right    { top: 24px; right: 100px; }
.slot-deco.pos-bottom-left  { bottom: 24px; left: 24px; }
.slot-deco.pos-bottom-right { bottom: 24px; right: 100px; }
.slot-deco.pos-left         { top: 50%; left: 24px; transform: translateY(-50%); }
.slot-deco.pos-right        { top: 50%; right: 100px; transform: translateY(-50%); }

/* Offstage — hidden */
.slot-offstage {
    opacity: 0;
    transform: scale(0.8);
    pointer-events: none;
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

/* ── Transition Animation States ── */

.slot-exiting {
    opacity: 0 !important;
    transform: scale(0.9) !important;
    transition: opacity 250ms ease, transform 250ms ease !important;
}

.slot-entering {
    opacity: 0;
    transform: scale(0.95);
}

/* will-change hint for compositing */
.stage-item:not(.slot-offstage) {
    will-change: opacity, transform;
}

/* ── Gallery Mode (Read mode) ── */

.stage-manager.stage-gallery {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    justify-content: center;
    align-items: flex-start;
    padding: 20px;
}

.slot-gallery {
    position: relative;
    width: calc(33.333% - 12px);
    max-height: 300px;
    opacity: 1;
    transform: none;
    pointer-events: auto;
}

/* ═══════════════════════════════════════════════════
   Phase 2: Stage Modal (Click-to-Inspect)
   ═══════════════════════════════════════════════════ */

.stage-modal-overlay {
    position: fixed;
    inset: 0;
    z-index: 200;
    background: rgba(0,0,0,0.85);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 300ms ease;
}
.stage-modal-overlay.visible {
    opacity: 1;
}

.stage-modal-inner {
    max-width: 90vw;
    max-height: 90vh;
}
.stage-modal-inner img,
.stage-modal-inner video {
    max-width: 90vw;
    max-height: 90vh;
    border-radius: var(--about-radius);
    object-fit: contain;
}

.stage-modal-close {
    position: absolute;
    /* welcome-walk P0-4: no JS mounts .stage-modal today (dead rule, kept for
       the toolkit's future), but a dismiss affordance in a fixed inset:0
       overlay is born with the safe-area inset or it is born a trap. */
    top: max(20px, env(safe-area-inset-top, 0px));
    right: 20px;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.3);
    background: rgba(0,0,0,0.6);
    color: white;
    font-size: calc(1.2rem * var(--type-scale, 1));
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 200ms;
    z-index: 201;
}
.stage-modal-close:hover {
    background: rgba(255,255,255,0.15);
    border-color: rgba(255,255,255,0.5);
}

/* ═══════════════════════════════════════════════════
   Phase 2: Scrub Bar (Timeline)
   ═══════════════════════════════════════════════════ */

.scrub-bar, .pres-scrub {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 60px;
    z-index: 75;
    padding: 6px 16px;
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 10px;
    background: rgba(15,15,26,0.85);
    border-top: 1px solid var(--about-border);
}

/* Play button inside scrub bar */
.pres-scrub-play.deck-ctrl-play,
.pres-scrub .deck-ctrl-play {
    width: 32px;
    height: 32px;
    min-width: 32px;
    border-radius: 50%;
    background: linear-gradient(135deg, var(--about-primary), var(--about-primary-end));
    border: none;
    color: white;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 200ms;
    box-shadow: 0 2px 8px var(--about-glow);
}
.pres-scrub .deck-ctrl-play:hover {
    transform: scale(1.08);
    box-shadow: 0 4px 16px var(--about-glow);
}

/* Track wrapper fills remaining space — also serves as enlarged touch target */
.scrub-track-wrap {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 10px 0;
    margin: -10px 0;
    cursor: pointer;
}

/* Time display inside scrub bar */
.pres-scrub-time.deck-time,
.pres-scrub .deck-time {
    font-size: calc(0.75rem * var(--type-scale, 1));
    color: var(--about-text-muted);
    min-width: 36px;
    text-align: right;
}

/* Maximize button in scrub bar */
.pres-scrub-maximize {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.15);
    background: rgba(255,255,255,0.06);
    color: rgba(255,255,255,0.7);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 200ms;
    margin-left: 4px;
    flex-shrink: 0;
}
.pres-scrub-maximize:hover {
    background: rgba(255,255,255,0.12);
    color: white;
    border-color: rgba(255,255,255,0.3);
}

.scrub-track {
    position: relative;
    height: 4px;
    background: rgba(255,255,255,0.1);
    border-radius: 2px;
    cursor: pointer;
    transition: height 150ms;
}
.scrub-track:hover,
.scrub-track.scrub-dragging {
    height: 6px;
}

.scrub-fill {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 0%;
    background: linear-gradient(90deg, var(--about-primary), var(--about-primary-end));
    border-radius: 2px;
    pointer-events: none;
}

.scrub-thumb {
    position: absolute;
    top: 50%;
    left: 0%;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--about-primary);
    transform: translate(-50%, -50%);
    opacity: 0;
    transition: opacity 200ms;
    pointer-events: none;
    box-shadow: 0 0 8px var(--about-glow);
}
.scrub-track:hover .scrub-thumb,
.scrub-track.scrub-dragging .scrub-thumb {
    opacity: 1;
}

.scrub-ticks {
    position: relative;
    height: 6px;
}
.scrub-tick {
    position: absolute;
    top: 0;
    width: 1px;
    height: 4px;
    background: rgba(255,255,255,0.2);
}

/* Frozen state (modal open) */
.scrub-bar.scrub-frozen, .pres-scrub.scrub-frozen {
    opacity: 0.4;
    pointer-events: none;
}

/* Hidden in read mode */
.read-mode .scrub-bar, .read-mode .pres-scrub {
    display: none;
}

/* ═══════════════════════════════════════════════════
   Phase 2: Landing Frame
   ═══════════════════════════════════════════════════ */

.landing-frame {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--about-bg);
}

.landing-inner {
    text-align: center;
    max-width: 700px;
    padding: 40px 20px;
}

.landing-title {
    font-size: clamp(2rem, 5vw, 3.5rem);
    font-weight: 700;
    line-height: 1.15;
    margin-bottom: 12px;
    background: linear-gradient(135deg, var(--about-text-bright), var(--about-primary));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
}

.landing-subtitle {
    font-size: calc(1.1rem * var(--type-scale, 1));
    color: var(--about-text-muted);
    margin-bottom: 32px;
}

.landing-chapters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: center;
    margin-bottom: 32px;
}

.landing-bubble {
    padding: 8px 16px;
    border-radius: 20px;
    border: 1px solid rgba(255,255,255,0.15);
    background: rgba(255,255,255,0.06);
    color: var(--about-text);
    font-size: calc(0.85rem * var(--type-scale, 1));
    cursor: pointer;
    transition: all 200ms;
}
.landing-bubble:hover {
    background: rgba(102,126,234,0.15);
    border-color: var(--about-primary);
    color: var(--about-text-bright);
}

.landing-list {
    text-align: left;
    max-width: 500px;
    margin: 0 auto 32px;
}

.landing-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid var(--about-border);
    cursor: pointer;
    transition: all 200ms;
}
.landing-row:hover {
    background: rgba(255,255,255,0.03);
    padding-left: 8px;
}

.landing-row-num {
    font-size: calc(0.8rem * var(--type-scale, 1));
    color: var(--about-primary);
    font-weight: 600;
    min-width: 24px;
}

.landing-row-title {
    flex: 1;
    font-size: calc(0.95rem * var(--type-scale, 1));
    color: var(--about-text);
}

.landing-row-dur {
    font-size: calc(0.8rem * var(--type-scale, 1));
    color: var(--about-text-muted);
    font-variant-numeric: tabular-nums;
}

.landing-countdown {
    font-size: calc(0.85rem * var(--type-scale, 1));
    color: var(--about-text-muted);
    /* No animation: Joe directive 2026-05-18 (A11) — no flashing text effects. */
}

/* Hide chapter header during landing */
.chapter-deck:has(.landing-frame.active-chapter) .chapter-header,
.pres-deck:has(.pres-landing.active) .pres-header {
    display: none;
}

/* ── Pause Continue Button Overlay ── */
.pres-countdown {
    display: none;
    position: absolute;
    bottom: 48px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 50;
    text-align: center;
    /* No fade-in: Joe directive 2026-05-18 (A11) — no flashing/fade-in text effects. */
}
.pres-countdown.active {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
}
.pres-countdown-message {
    font-size: calc(0.95rem * var(--type-scale, 1));
    color: var(--about-text-muted, #aaa);
    max-width: 400px;
}
.pres-countdown-skip {
    padding: 12px 36px;
    font-size: calc(1.1rem * var(--type-scale, 1));
    font-weight: 600;
    color: #fff;
    background: linear-gradient(135deg, #667eea, #764ba2);
    border: none;
    border-radius: 28px;
    cursor: pointer;
    box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
    transition: transform 0.2s, box-shadow 0.2s;
}
.pres-countdown-skip:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 24px rgba(102, 126, 234, 0.6);
}
.pres-countdown-skip:active {
    transform: translateY(0);
}
/* @keyframes fadeSlideUp removed 2026-05-18 (A11 — no flashing/fade-in text effects). */

/* ═══════════════════════════════════════════════════
   Unified Engine: Scene Stage + Media Items
   ═══════════════════════════════════════════════════ */



/* Media items */
.media-item {
    position: relative;
    border-radius: var(--about-radius);
    overflow: hidden;
    background: var(--about-bg-card);
    transition: opacity 0.4s ease, transform 0.4s ease;
}
.media-item video, .media-item img, .media-item iframe.yt-embed {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    border: none;
}
.mi-yt-frame {
    width: 100%;
    height: 100%;
    border: none;
}
.media-item.placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px dashed rgba(102,126,234,0.3);
    min-height: 120px;
}
.media-placeholder-label {
    font-size: calc(0.85rem * var(--type-scale, 1));
    color: var(--about-text-muted);
    text-align: center;
    padding: 16px;
}



/* ClipPlayer tap-to-play overlay (autoplay-blocked scenarios) */
.clip-player-tap-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0,0,0,0.6);
    z-index: 11;
    cursor: pointer;
}
.clip-player-tap-text {
    color: #fff;
    font-size: calc(1.25rem * var(--type-scale, 1));
    font-weight: 600;
    padding: 12px 24px;
    background: rgba(255,255,255,0.15);
    border-radius: 8px;
    backdrop-filter: blur(4px);
}

/* ── Document thumbnails in MediaPool ── */
/* Paper-page aesthetic: title-forward design, visually distinct from photos/videos */
.media-doc-thumb {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    height: 100%;
    min-height: 80px;
    background: linear-gradient(165deg, #f5f0e8 0%, #ede6d6 60%, #e8dfc8 100%);
    position: relative;
    padding: 0;
    overflow: hidden;
}
/* Dog-ear corner fold */
.media-doc-thumb::before {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    width: 20px;
    height: 20px;
    background: linear-gradient(225deg, var(--about-bg-card) 50%, #d4cbb8 50%, #c9bfa8 100%);
    z-index: 2;
}
/* Subtle ruled lines */
.media-doc-thumb::after {
    content: '';
    position: absolute;
    top: 28px;
    left: 10px;
    right: 10px;
    bottom: 10px;
    background: repeating-linear-gradient(
        to bottom,
        transparent,
        transparent 13px,
        rgba(180, 165, 140, 0.25) 13px,
        rgba(180, 165, 140, 0.25) 14px
    );
    pointer-events: none;
    z-index: 0;
}
/* DOC type badge */
.media-doc-icon {
    position: absolute;
    top: 5px;
    left: 6px;
    font-size: 0;
    z-index: 1;
}
.media-doc-icon::after {
    content: 'DOC';
    font-size: calc(0.55rem * var(--type-scale, 1));
    font-weight: 700;
    letter-spacing: 0.05em;
    color: #8b7355;
    background: rgba(139, 115, 85, 0.12);
    padding: 1px 5px;
    border-radius: 3px;
}
/* Title as hero element */
.media-doc-label {
    position: relative;
    z-index: 1;
    font-size: calc(0.78rem * var(--type-scale, 1));
    font-weight: 600;
    color: #3d3424;
    line-height: 1.35;
    padding: 20px 12px 12px;
    text-align: center;
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
    word-break: break-word;
}
/* Compact in wing panels */
.tww-left-wing .media-doc-thumb,
.tww-right-wing .media-doc-thumb {
    min-height: 60px;
}
.tww-left-wing .media-doc-thumb::before,
.tww-right-wing .media-doc-thumb::before {
    width: 14px;
    height: 14px;
}
.tww-left-wing .media-doc-label,
.tww-right-wing .media-doc-label {
    font-size: calc(0.65rem * var(--type-scale, 1));
    padding: 16px 6px 8px;
    -webkit-line-clamp: 3;
}

/* ── Document cover image thumbnail (preferred over fallback) ── */
.media-doc-cover {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}
/* DOC badge overlay on cover image thumbnails */
.media-doc-badge {
    position: absolute;
    top: 5px;
    left: 6px;
    font-size: calc(0.55rem * var(--type-scale, 1));
    font-weight: 700;
    letter-spacing: 0.05em;
    color: rgba(255, 255, 255, 0.95);
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    padding: 2px 6px;
    border-radius: 3px;
    z-index: 2;
    pointer-events: none;
}

/* ── Media Inspect Modal (half-pane, two-panel) ── */
.media-inspect-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 300ms;
}
.media-inspect-overlay.visible {
    opacity: 1;
}
.media-inspect-modal {
    width: calc((100vw - var(--side-nav-width, 60px)) * 0.85);
    max-width: 95%;
    height: calc((100vh - var(--nav-height, 60px)) * 0.85);
    max-height: 95%;
    min-width: 400px;
    min-height: 300px;
    display: flex;
    flex-direction: row;
    border-radius: var(--about-radius-lg, 16px);
    overflow: hidden;
    background: rgba(15, 15, 26, 0.95);
    border: 1px solid var(--about-border, rgba(255,255,255,0.1));
    box-shadow: 0 16px 64px rgba(0, 0, 0, 0.6);
}

/* Left panel: media display */
.mi-media-panel {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.4);
    overflow: hidden;
    min-width: 0;
}
.mi-media-panel img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}
.mi-media-panel video {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}
.mi-pdf-frame {
    width: 100%;
    height: 100%;
    border: none;
}

/* Right panel: metadata + controls */
.mi-info-panel {
    width: 260px;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    border-left: 1px solid rgba(255, 255, 255, 0.1);
}
.mi-header {
    padding: 12px 16px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    flex-shrink: 0;
}
.mi-title {
    font-size: calc(14px * var(--type-scale, 1));
    font-weight: 600;
    color: white;
    margin: 0 0 8px 0;
    word-break: break-word;
}
.mi-controls {
    display: flex;
    gap: 6px;
}
.mi-ctrl-btn {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.3);
    background: rgba(0, 0, 0, 0.4);
    color: white;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    padding: 0;
    transition: all 200ms;
}
.mi-ctrl-btn:hover {
    background: rgba(255, 255, 255, 0.15);
    border-color: white;
}
.mi-close {
    margin-left: auto;
}
.mi-body {
    flex: 1;
    overflow-y: auto;
    padding: 12px 16px;
    -webkit-overflow-scrolling: touch;
}
.mi-section {
    margin-bottom: 16px;
}
.mi-section label {
    display: block;
    font-size: calc(11px * var(--type-scale, 1));
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: rgba(255, 255, 255, 0.5);
    margin-bottom: 6px;
}
.mi-section p {
    margin: 0;
    font-size: calc(13px * var(--type-scale, 1));
    line-height: 1.5;
    color: rgba(255, 255, 255, 0.85);
}
.mi-meta-row {
    display: flex;
    align-items: center;
    gap: 10px;
}
.mi-type-badge {
    display: inline-block;
    padding: 3px 10px;
    background: rgba(139, 92, 246, 0.2);
    border-radius: 12px;
    font-size: calc(11px * var(--type-scale, 1));
    font-weight: 600;
    text-transform: uppercase;
    color: #a78bfa;
}
.mi-date {
    font-size: calc(13px * var(--type-scale, 1));
    color: rgba(255, 255, 255, 0.6);
}
.mi-analysis {
    background: rgba(0, 0, 0, 0.2);
    padding: 12px;
    border-radius: 8px;
    border-left: 3px solid #8b5cf6;
    font-size: calc(13px * var(--type-scale, 1));
    line-height: 1.5;
    color: rgba(255, 255, 255, 0.85);
}

/* Mobile: stack vertically */
@media (max-width: 768px) {
    .media-inspect-modal {
        width: 90vw;
        height: 80vh;
        min-width: unset;
        flex-direction: column;
    }
    .mi-media-panel {
        max-height: 50%;
    }
    .mi-info-panel {
        width: 100%;
        border-left: none;
        border-top: 1px solid rgba(255, 255, 255, 0.1);
    }
}

/* ═══════════════════════════════════════════════════
   Responsive Additions
   ═══════════════════════════════════════════════════ */

@media (max-width: 768px) {
    .pres-header .pres-scrub {
        gap: 6px;
    }

    .landing-inner { padding: 20px 16px; }
    .landing-bubble { padding: 6px 12px; font-size: calc(0.8rem * var(--type-scale, 1)); }
}

@media (max-width: 480px) {
}


/* ═══════════════════════════════════════════════════
   Touch hit-area dressing — unbifurcating-vault-surfaces S5
   Coarse-pointer devices only: real phones/tablets, INCLUDING the projected
   frame (pointer:coarse resolves from the device while every viewport MQ
   resolves from the 1600×900 design box). Pure hit-area expansion via
   transparent ::after overlays + touch-action — zero layout change, zero
   visual change; fine-pointer desktop is untouched by the MQ.
   S1 survey basis: the 24–36px transport controls land at 10–17pt effective
   at projection k≈0.42–0.48, far under the 44pt floor. Expansion is bounded
   by the real inter-control gaps (half-gap on the crowded axis) so adjacent
   hit zones never invert; the free axis gets the reach.
   ═══════════════════════════════════════════════════ */
@media (pointer: coarse) {
    .deck-ctrl-play, .deck-ctrl-prev, .deck-ctrl-next, .mi-ctrl-btn,
    .narrative-toggle, .pres-header-narration,
    .pres-header .ffa-exit-btn, .pres-scrub-maximize {
        position: relative;
        touch-action: manipulation;
    }
    .deck-ctrl-play::after, .deck-ctrl-prev::after, .deck-ctrl-next::after,
    .mi-ctrl-btn::after, .narrative-toggle::after,
    .pres-header-narration::after,
    .pres-header .ffa-exit-btn::after, .pres-scrub-maximize::after {
        content: '';
        position: absolute;
        inset: -8px -4px;
    }
    /* Deck column (fixed right, isolated horizontally; 8px vertical gap) */
    .deck-controls .deck-ctrl-prev::after,
    .deck-controls .deck-ctrl-next::after,
    .deck-controls .deck-ctrl-play::after { inset: -4px -14px; }
    .deck-controls .narrative-toggle::after { inset: -2px -14px; }
    /* Media-inspector row: 6px gaps — half-gap horizontal, free vertical */
    .mi-controls .mi-ctrl-btn::after { inset: -8px -3px; }
    /* Header row: 12px gaps */
    .pres-header-narration::after,
    .pres-header .ffa-exit-btn::after { inset: -8px -6px; }
    /* Scrub bar: the track wrap already carries a padding/negative-margin
       touch band (10px) — widen it; taps just above the bar land on the
       track instead of the stage. */
    .scrub-track-wrap { padding: 22px 0; margin: -22px 0; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   START GATE — SECOND HOME (truing-surfing-goldstandard S3, 2026-08-17)

   ⚠️ THESE RULES ALSO EXIST IN `app/about/about.css`, DELIBERATELY, AND THAT
   IS A DECLARED DUPLICATE — NOT AN OVERSIGHT. Read this before "tidying" it.

   The gate is a presentation-ENGINE affordance: `PresentationPlayer.armStartGate`
   builds `.pres-start-gate` for any player constructed with `{startGate:true}`.
   Its CSS, however, was written only into `about.css`, because /about/ was the
   only surface that armed it. S3 arms it on the Surfing story as well (W-2 — the
   welcome chain's essay leg must never land the member on a dead player), and
   the consequence was MEASURED in the browser rather than assumed: with the
   narration refused, `.pres-start-gate` computed to `position: static;
   z-index: auto`, laid out in normal flow at 1440x154, with a cue-bundle iframe
   taking the pointer events over its middle. The member's one way into the
   story was unstyled and unclickable.

   The obvious fix — MOVE the block here — was tried first and reverted, because
   /about/ does not load this file. `about.css` is not a companion to
   presentation.css; it is a PARALLEL FORK of it, and the two pages load one
   each. Moving the rules would have styled the story pages by un-styling the
   vault's front door.

   So the block is duplicated, knowingly, at the smallest scope that fixes the
   defect without touching a Joe-praised surface. The real repair is to end the
   about.css / presentation.css fork, which is a refactor of its own and is
   routed to the S4 SR ledger rather than smuggled into a welcome-rail session.
   Until then: EDIT BOTH COPIES.
   ═══════════════════════════════════════════════════════════════════════════ */
/* ═══════════════════════════════════════════════════════════════════════════
   START GATE — the first frame's one unmistakable Play affordance
   (tuning-welcome-process S4)

   Two silences, one remedy. A phone opens the stage PAUSED at pure f(0) by
   design (PresentationPlayer LS-1 — never silent runaway playback), and a
   desktop browser refuses narration audio to a member who arrived from an
   emailed invitation link with no gesture to spend. Both leave a first-time
   member looking at a still frame with no sound, which reads as broken. This
   is what says "press here", once, over the first frame only.

   Opt-in per player (`new PresentationPlayer(el, manifest, {startGate:true})`),
   so it exists on the About surface and nowhere else. z-index sits above the
   stage (1–90 in this file) and below --z-modal-umi (10400), because the UMI
   and every other modal must still open over it.
   ═══════════════════════════════════════════════════════════════════════════ */
.pres-start-gate {
    position: fixed;
    inset: 0;
    z-index: 9000;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    border: 0;
    /* Not opaque: the authored first frame is the point, and the member should
       see the story they are about to start, not a curtain over it. */
    background: radial-gradient(ellipse at center,
                rgba(15,15,26,0.55) 0%, rgba(15,15,26,0.88) 100%);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    animation: pres-start-gate-in 500ms cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes pres-start-gate-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

.pres-start-gate-inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    text-align: center;
    padding: 0 32px;
    max-width: 420px;
}

.pres-start-gate-btn {
    width: 104px;
    height: 104px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--about-text-bright);
    background: linear-gradient(135deg, var(--about-primary), var(--about-primary-end));
    box-shadow: 0 0 0 0 var(--about-glow), 0 12px 40px rgba(0,0,0,0.45);
    /* The play triangle is optically centred, not geometrically centred. */
    padding-left: 8px;
    /* G-088 (Greg 2026-10-18): six pulses (~16 s), then the gate rests. `infinite` kept an idle, paused
       page animating forever (battery) and meant iOS never reported "animations complete", so every
       XCUITest finger press waited 60 s for idle. The invitation is made; the halo needn't nag. */
    animation: pres-start-gate-pulse 2600ms ease-out 6;
}
@keyframes pres-start-gate-pulse {
    0%   { box-shadow: 0 0 0 0 var(--about-glow), 0 12px 40px rgba(0,0,0,0.45); }
    70%  { box-shadow: 0 0 0 26px rgba(102,126,234,0), 0 12px 40px rgba(0,0,0,0.45); }
    100% { box-shadow: 0 0 0 0 rgba(102,126,234,0), 0 12px 40px rgba(0,0,0,0.45); }
}

.pres-start-gate-label {
    font-size: 1.45rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--about-text-bright);
}

.pres-start-gate-sub {
    font-size: 0.95rem;
    line-height: 1.5;
    color: var(--about-text-muted);
}

/* ── The gate's secondary choices (Return 068 P0-5) ─────────────────────────
   The gate covers the header, so Exit and Fergi were unreachable until Play
   was pressed. These restore the choice WITHOUT competing with Play: smaller,
   quieter, below the sub-line, and separated by a rule. Play remains the one
   thing the eye lands on — colour and mass both say so. */
.pres-start-gate-alts {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    margin-top: 6px;
    padding-top: 14px;
    border-top: 1px solid rgba(255, 255, 255, 0.13);
    width: 100%;
}

.pres-start-gate-duration {
    font-size: 0.82rem;
    letter-spacing: 0.04em;
    color: var(--about-text-muted);
    opacity: 0.85;
}

.pres-start-gate-alt-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
}

.pres-start-gate-alt {
    /* Deliberately a text-weight control, not a second call to action. */
    background: transparent;
    border: 1px solid rgba(255, 255, 255, 0.22);
    border-radius: 999px;
    padding: 7px 15px;
    font: inherit;
    font-size: 0.85rem;
    color: var(--about-text-muted);
    cursor: pointer;
    transition: border-color 160ms ease, color 160ms ease, background-color 160ms ease;
}

.pres-start-gate-alt:hover,
.pres-start-gate-alt:focus-visible {
    color: var(--about-text-bright);
    border-color: rgba(255, 255, 255, 0.45);
    background: rgba(255, 255, 255, 0.06);
}

.pres-start-gate-alt:focus-visible {
    outline: 2px solid var(--about-accent);
    outline-offset: 2px;
}

@media (pointer: coarse) {
    /* A thumb needs a real target; 44px is the floor. */
    .pres-start-gate-alt { padding: 11px 18px; font-size: 0.9rem; }
}

/* Keyboard reach — the gate is a real button and must look like one when
   tabbed to, not only when moused. */
.pres-start-gate:focus-visible {
    outline: 2px solid var(--about-accent);
    outline-offset: -4px;
}

@media (pointer: coarse) {
    /* A thumb, not a cursor. */
    .pres-start-gate-btn { width: 116px; height: 116px; }
}

/* ── THE BARE GATE (interpreting-family-channels S4, ruling R-D) ──────────────
   Off the welcome rail the gate is ONE Play disc over the first frame. The
   full-window layer stays (it is what centres the disc and sits above the
   stage), but it takes no pointer events and draws only a faint vignette, so
   the story's own chrome — the rail's Say It, its Fergi AI mark, Exit, the
   Pocket top bar — stays topmost and pressable while the story waits.
   Only the disc and its label are the Play target. EDIT BOTH COPIES
   (presentation.css + about.css — see the duplicate note above). */
.pres-start-gate[data-variant="bare"] {
    pointer-events: none;
    cursor: default;
    background: radial-gradient(ellipse at center,
                rgba(15,15,26,0.28) 0%, rgba(15,15,26,0) 70%);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}
.pres-start-gate[data-variant="bare"] .pres-start-gate-inner {
    pointer-events: auto;
    cursor: pointer;
    gap: 10px;
}

@media (prefers-reduced-motion: reduce) {
    .pres-start-gate { animation: none; }
    .pres-start-gate-btn { animation: none; }
}

/* ── Desktop story surround (2026-10-03, StorySurround.js) ──────────────────
   ⚠ MIRROR: StorySurround.js injects these same rules itself (pages that load only
   /about/about.css need them); its copy is canonical and wins the cascade. This block stays
   only so a cached older StorySurround.js still finds its rules — edit the JS copy.
   An authored field around the fixed 1600×900 desktop stage. Desktop only: the
   class .ffa-surround-on is set by StorySurround after a story's surround.json
   loads, and StorySurround is never constructed on the Pocket stage. The field
   sits behind every card; design-stage cards and their letterbox containers go
   transparent so it shows in BOTH the card padding and the letterbox, while the
   opaque .cue-stage-backing (cue-bundle.js) keeps it strictly off the stage. */
.ffa-surround {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
    contain: strict;
    transition: background-color 900ms ease;
}
.ffa-surround-still,
.ffa-surround-video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    transition: opacity 1200ms ease;
}
.ffa-surround-still {
    background-size: cover;
    background-repeat: no-repeat;
}
.ffa-surround-video { object-fit: cover; }
.ffa-surround-still.is-on,
.ffa-surround-video.is-on { opacity: 1; }
.pres-deck.ffa-surround-on .pres-card:has(.cue-design-stage) { background: transparent !important; }
.pres-deck.ffa-surround-on .cue-design-stage { background: transparent !important; }
/* The bio-story crossfade backdrop (.story-bio .visual-frame.active::before) exists to occlude
   an outgoing frame's title. For a design-stage frame the opaque .cue-stage-backing inside the
   ACTIVE frame (z-above the outgoing one) already occludes the whole stage, and an outgoing
   frame's letterbox is transparent, so under the surround only the stage needs covering. */
.pres-deck.ffa-surround-on .visual-frame.active.cue-design-stage::before { background: transparent; }
@media (prefers-reduced-motion: reduce) {
    .ffa-surround, .ffa-surround-still, .ffa-surround-video { transition: none; }
}
.pres-header-surround {
    background: rgba(255,255,255,0.06);
    border: 1px solid rgba(255,255,255,0.14);
    color: inherit;
    font: inherit;
    font-size: 12px;
    padding: 4px 9px;
    border-radius: 6px;
    cursor: pointer;
    white-space: nowrap;
}
.pres-header-surround:hover { background: rgba(255,255,255,0.12); }
.pres-header-surround:focus-visible { outline: 2px solid var(--about-primary, #667eea); outline-offset: 2px; }
/* The right-hand cue rail (.pres-controls, position:fixed, a sibling of the deck) sits ON the
   surround once one is shown — give it its own readable surface. Extended by box-shadow, not
   padding, so no button moves and no tap target changes. */
body:has(.pres-deck.ffa-surround-on) .pres-controls {
    background: rgba(9, 17, 23, 0.82);
    border-radius: 16px;
    box-shadow: 0 0 0 8px rgba(9, 17, 23, 0.82), 0 0 0 9px rgba(255, 255, 255, 0.07);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
}
