/* ═══════════════════════════════════════════════════════════════
   ANIMATION TIMELINE — CSS-driven scrub/play for book animations
   Uses animation-play-state: paused + negative animation-delay
   to freeze any animation at an exact position via CSS variables.
   Loads after storybook-unified.css, before design-editor.css.

   Modes: open, close, pickup, setdown, flyleaf, pageturn
   Mode is set via data-scrub-mode / data-play-mode attributes
   on .book-desk-scene (managed by editor-interop.js).
   ═══════════════════════════════════════════════════════════════ */

/* ── Timeline CSS variables (defaults) ── */
.book-desk-scene {
    --timeline-cover: 0;
    --timeline-frame: 0;
    --timeline-width: 0;
    --timeline-depth: 0;
    --timeline-info: 0;
    --timeline-close-fly1: 0;
    --timeline-close-fly2: 0;
    --timeline-close-fly3: 0;
    --timeline-close-fly4: 0;
    --timeline-close-cover: 0;
    --timeline-close-frame: 0;
    --timeline-close-width: 0;
    --timeline-close-info: 0;
    --timeline-close-depth: 0;
    --timeline-fly1: 0;
    --timeline-fly2: 0;
    --timeline-fly3: 0;
    --timeline-fly4: 0;
    --timeline-open-fly1: 0;
    --timeline-open-fly2: 0;
    --timeline-open-fly3: 0;
    --timeline-open-fly4: 0;
    --timeline-pickup-frame: 0;
    --timeline-pickup-glow: 0;
    --timeline-setdown-frame: 0;
    --timeline-setdown-glow: 0;
    --timeline-active-perspective: 1200px;
}

/* ═══════════════════════════════════════════════════════════════
   KEYFRAMES — reused across scrub and play modes
   ═══════════════════════════════════════════════════════════════ */

/* Track: Frame rotation — reuses the runtime @keyframes frameOpenSettle
   defined in desk-scene.css. Any edits to the runtime keyframe
   automatically propagate to scrub. No duplicate definition needed. */

/* Track: Frame width — single page → spread (mobile/tablet: no change) */
@keyframes timelineFrameWidthDefault {
    0%   { width: min(var(--book-frame-max-w), 50vw); }
    100% { width: min(var(--book-frame-max-w), 50vw); }
}

/* Track: Depth container — visible throughout opening, transitions
   from no Z-push (closed) to full backward push (open) to prevent
   Z-fighting with the viewport background as it becomes visible.
   Runtime: desk-scene.css pushes .book-3d-depth via translateZ in
   Opening/Settling/Book states but not Cover/Table states.
   NOTE: No longer used by scrub mode (calc() interpolation instead)
   because CSS animation on a preserve-3d container causes Chromium
   to flatten children's 3D transforms. Retained for future play mode. */
@keyframes timelineDepthFade {
    0%   { opacity: 1; transform: translateZ(0px); }
    100% { opacity: 1; transform: translateZ(calc(-1 * (var(--book-open-page-depth) + 2px))); }
}

/* Track 1 (Open): Cover swing — reuses existing @keyframes coverPageOpen */

/* Track 5 (Open): Info panel reveal — was missing, causing the info panel
   to be forced visible at all scrub positions on desktop. Now animates from
   hidden (no layout impact) to fully visible. */
/* Snap reveal — the inside content page should appear naturally as the
   last flyleaf turns past it, not fade in gradually. The 0→1 snap at
   1% means the panel appears almost instantly once its track starts.
   Track timing is synced to the end of fly4 cascade (master ~75%). */
@keyframes infoPanelReveal {
    0%   { opacity: 0; visibility: hidden; }
    1%   { opacity: 1; visibility: visible; }
    100% { opacity: 1; visibility: visible; }
}

/* Nav/ribbon elements hidden during animation, revealed at the end
   to match Book state where they're opacity:1 + pointer-events:auto. */
@keyframes navReveal {
    0%, 90% { opacity: 0; visibility: hidden; pointer-events: none; }
    100%    { opacity: 1; visibility: visible; pointer-events: auto; }
}

/* pageRevealOpen — DEAD CODE (pages now immediately visible in play mode).
   Cover z-index:100 and flyleaves z-index:90-66 naturally occlude pages z-index:50
   in preserve-3d, making the delayed reveal unnecessary. Kept to prevent
   missing-keyframe errors from any residual references. */
@keyframes pageRevealOpen {
    0%   { opacity: 0; visibility: hidden; }
    100% { opacity: 1; visibility: visible; }
}

/* hardcoverReveal — DEAD CODE (hardcover globally hidden).
   Kept to prevent missing-keyframe errors from any residual references. */
@keyframes hardcoverReveal {
    0%   { opacity: 0; }
    100% { opacity: 0; }
}

/* Desktop-only: Frame width keyframes with actual spread values */
@media (min-width: 1200px) {
    @keyframes timelineFrameWidthDesktop {
        0%   { width: min(800px, 42vw); }
        100% { width: min(1600px, 85vw); }
    }

    /* Desktop: Frame-right shrinks from 100% (closed book) to 50% (open spread)
       as the frame widens. margin-left is animated from 0% to 50% instead of using
       auto — CSS can interpolate percentage values smoothly, and this keeps the left
       edge pinned to the spine (x=0) during the cover swing, preventing the visible
       gap that auto creates under 3D perspective. */
    @keyframes timelineFrameRightWidth {
        0%   { width: 100%; margin-left: 0%; }
        100% { width: 50%; margin-left: 50%; }
    }

    /* Desktop back cover open: constrain left from 0 (full frame) to 50% (right half).
       Linear 0→50% matches frame-right's margin-left 0→50% (timelineFrameRightWidth).
       Same delay/duration in play-mode ensures both are at the same keyframe % at every
       moment, preventing the back cover from extending into the left half of the frame. */
    @keyframes timelineDepthBackCoverOpen {
        0%   { left: 0; }
        100% { left: 50%; }
    }
}

/* Track: Left-stack reveal — turned-page slab appears as cover opens.
   Matches desk-scene.css Opening state (.book-3d-left-stack opacity/transform). */
@keyframes timelineLeftStackReveal {
    0%   { opacity: 0; transform: translateZ(0); }
    30%  { opacity: 0; transform: translateZ(0); }
    100% { opacity: 1; transform: translateZ(calc(var(--book-open-page-depth) * 0.3)); }
}

/* Track: Ribbon fade — ribbons disappear as book opens.
   Complete fade by 20% of depth so ribbons are invisible before depth
   elements begin transitioning at 30% (preventing stretch artifacts). */
@keyframes timelineRibbonFade {
    0%   { opacity: 1; }
    20%  { opacity: 0; }
    100% { opacity: 0; }
}

/* Track: Ribbon reveal — ribbons reappear as book closes.
   Appear at 80% of close-depth (reverse of 20% open fade). */
@keyframes timelineRibbonReveal {
    0%   { opacity: 0; }
    80%  { opacity: 0; }
    100% { opacity: 1; }
}

/* ── Depth geometry keyframes — closed → open transitions ──────────
   Replicate desk-scene.css settling/book state geometry so the scrub
   can smoothly transition depth children from closed to open positions.
   IMPORTANT: Spine keyframe must NOT include transform — the base CSS
   rotateY(-90deg) would be wiped. Only right/width are animated. */

/* Pages: full-width → right-half, Z from closed-depth to open-depth.
   Open state includes slight rotateY tenting (pages angle up from spine).
   Hold at closed positions until 30% of depth track so pages begin
   transitioning as the cover nears flat (~-151deg at master ~34.5%),
   synced with the frame width expansion (master 35-59%). The previous
   65% hold caused a 12% master-time gap where the frame expanded but
   depth elements remained frozen, visually stretching the back cover
   and exposing page/spine clipping. */
@keyframes timelineDepthPage {
    0%   { opacity: 0; left: 0;   right: var(--book-page-inset-right);
           transform: translateZ(calc(var(--book-closed-page-depth) * var(--layer-i) / var(--book-3d-layer-total))); }
    30%  { opacity: 0; left: 0;   right: var(--book-page-inset-right);
           transform: translateZ(calc(var(--book-closed-page-depth) * var(--layer-i) / var(--book-3d-layer-total))); }
    /* Page layers stay invisible throughout the entire scrub/play timeline.
       In scrub mode the viewport is transparent, so any visible page layer
       clips through the viewport content (visible as grey/orange tint on
       the right page at 49-63%). In play mode, the scrub rules apply
       opacity: 1 !important which overrides this, and the cream viewport
       background hides the layers naturally. Position/transform still
       animate so geometry is correct if layers are force-shown. */
    100% { opacity: 0; left: calc(50% + 4px); right: 0;
           transform: translateZ(calc(var(--book-open-page-depth) * var(--layer-i) / var(--book-3d-layer-total) - var(--book-page-tent-clearance, 12px)))
                      rotateY(var(--book-open-page-tent, 3deg)); }
}

/* Spine: left edge → center, width expands to open-spine-width.
   Only right & width — transform stays on the base rule.
   Uses --book-open-spine-width for the wider rounded binding.
   The 3D spine stays visible throughout most of the opening so the
   beautiful leather detail isn't replaced by a flat fallback. Opacity
   fades to 0 only at 85-100% of depth track, by which point the
   viewport cream background and cover naturally occlude the spine. */
@keyframes timelineDepthSpine {
    0%   { right: 100%; width: calc(var(--book-spine-thickness) + 2 * var(--book-cover-board-thickness) + 2px); opacity: 1; }
    6%   { right: 100%; width: calc(var(--book-spine-thickness) + 2 * var(--book-cover-board-thickness) + 2px); opacity: 1; }
    30%  { right: 100%; width: calc(var(--book-spine-thickness) + 2 * var(--book-cover-board-thickness) + 2px); opacity: 1; }
    85%  { right: 50%;  width: var(--book-open-spine-width, var(--book-open-page-depth)); opacity: 1; }
    100% { right: 50%;  width: var(--book-open-spine-width, var(--book-open-page-depth)); opacity: 0; }
}

/* Back cover: stays visible throughout opening. Transform transitions
   from board-thickness Z (closed) to -1px (open/flat). Opacity remains 1
   so the 3D back cover provides the leather border in the open state. */
@keyframes timelineDepthBackCover {
    0%   { opacity: 1; transform: translateZ(var(--book-cover-board-thickness, 6px)); }
    30%  { opacity: 1; transform: translateZ(var(--book-cover-board-thickness, 6px)); }
    100% { opacity: 1; transform: translateZ(-1px); }
}

/* Right edge: width narrows progressively as each flyleaf departs the stack.
   Steps at 6%/29%/49%/69% of depth track = flyleaf 1/2/3/4 departure points.
   Each flyleaf ≈ 8% of the 13-layer stack → factors 0.92, 0.84, 0.76.
   Opacity fades 6-30% so edges don't project through flyleaves or viewport. */
@keyframes timelineDepthEdgeRight {
    0%   { width: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
    6%   { width: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
    29%  { width: calc(var(--book-closed-page-depth) * 0.92); opacity: 0; }
    49%  { width: calc(var(--book-closed-page-depth) * 0.84); opacity: 0; }
    69%  { width: calc(var(--book-closed-page-depth) * 0.76); opacity: 0; }
    100% { width: var(--book-open-page-depth); opacity: 0; }
}

/* Top edge: progressive height + left shift tracking flyleaf departures.
   Same opacity fade as right edge. */
@keyframes timelineDepthEdgeTop {
    0%   { left: 0;    height: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
    6%   { left: 0;    height: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
    29%  { left: 7%;   height: calc(var(--book-closed-page-depth) * 0.92); opacity: 0; }
    49%  { left: 15%;  height: calc(var(--book-closed-page-depth) * 0.84); opacity: 0; }
    69%  { left: 28%;  height: calc(var(--book-closed-page-depth) * 0.76); opacity: 0; }
    100% { left: 50%;  height: var(--book-open-page-depth); opacity: 0; }
}

/* Bottom edge: same progressive thinning + left shift + opacity as top edge. */
@keyframes timelineDepthEdgeBottom {
    0%   { left: 0;    height: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
    6%   { left: 0;    height: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
    29%  { left: 7%;   height: calc(var(--book-closed-page-depth) * 0.92); opacity: 0; }
    49%  { left: 15%;  height: calc(var(--book-closed-page-depth) * 0.84); opacity: 0; }
    69%  { left: 28%;  height: calc(var(--book-closed-page-depth) * 0.76); opacity: 0; }
    100% { left: 50%;  height: var(--book-open-page-depth); opacity: 0; }
}

/* ── Reverse depth geometry keyframes — open → closed ──────────
   Used by the close scrub to morph depth children back. */

/* Close keyframes: hold at open positions for first 70%, transition in remaining 30%.
   Reverse of open's 30% hold pattern — geometry snaps back in the final third
   of the close, after the frame has contracted and the cover is nearly shut. */
@keyframes timelineDepthPageClose {
    0%   { opacity: 0; left: calc(50% + 4px); right: 0;
           transform: translateZ(calc(var(--book-open-page-depth) * var(--layer-i) / var(--book-3d-layer-total) - var(--book-page-tent-clearance, 12px)))
                      rotateY(var(--book-open-page-tent, 3deg)); }
    /* Page layers invisible throughout close — same rationale as open. */
    70%  { opacity: 0; left: 0; right: var(--book-page-inset-right);
           transform: translateZ(calc(var(--book-closed-page-depth) * var(--layer-i) / var(--book-3d-layer-total))); }
    100% { opacity: 0; left: 0;   right: var(--book-page-inset-right);
           transform: translateZ(calc(var(--book-closed-page-depth) * var(--layer-i) / var(--book-3d-layer-total))); }
}

/* Close spine: fade-in at 0-15% + animated Z-compensation (pd+2 → 0 by 70%).
   Unlike the old keyframe which animated right/width (causing a horizontal slide),
   the spine now stays at its base CSS closed position (right: 100%, closed width)
   throughout — matching the scrub mode behavior. The Z-compensation decreases in
   sync with the depth container's Z-push removal so net spine Z stays at 0. */
@keyframes timelineDepthSpineClose {
    0% {
        opacity: 0;
        transform:
            translateZ(calc(var(--book-open-page-depth) + 2px))
            translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
            translateY(var(--book-spine-extra-ty, 0px))
            translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
            rotateX(var(--book-spine-extra-rx, 0deg))
            rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
            rotateZ(var(--book-spine-extra-rz, 0deg));
    }
    15% {
        opacity: 1;
        transform:
            translateZ(calc(var(--book-open-page-depth) + 2px))
            translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
            translateY(var(--book-spine-extra-ty, 0px))
            translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
            rotateX(var(--book-spine-extra-rx, 0deg))
            rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
            rotateZ(var(--book-spine-extra-rz, 0deg));
    }
    70% {
        opacity: 1;
        transform:
            translateZ(0px)
            translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
            translateY(var(--book-spine-extra-ty, 0px))
            translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
            rotateX(var(--book-spine-extra-rx, 0deg))
            rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
            rotateZ(var(--book-spine-extra-rz, 0deg));
    }
    100% {
        opacity: 1;
        transform:
            translateZ(0px)
            translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
            translateY(var(--book-spine-extra-ty, 0px))
            translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
            rotateX(var(--book-spine-extra-rx, 0deg))
            rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
            rotateZ(var(--book-spine-extra-rz, 0deg));
    }
}

/* Back cover close: Z-depth + position animation.
   0-20% (total 50-60%): hold at open-state position while frame-right hasn't started.
   20-100% (total 60-100%): left contracts from 50% to 0, synced with frame-right expansion.
   70-100%: Z transitions from open (-1px) to closed (board-thickness).
   top/right/bottom animate from open-state extended overhang to base overhang. */
@keyframes timelineDepthBackCoverClose {
    0%   { opacity: 1; transform: translateZ(-1px);
           top: calc(-1 * var(--book-cover-overhang-top) - 6px);
           right: calc(-1 * var(--book-cover-overhang-right) - 8px);
           bottom: calc(-1 * var(--book-cover-overhang-bottom) - 6px); }
    20%  { opacity: 1; transform: translateZ(-1px); }
    70%  { opacity: 1; transform: translateZ(-1px); }
    100% { opacity: 1; transform: translateZ(var(--book-cover-board-thickness, 6px));
           top: calc(-1 * var(--book-cover-overhang-top));
           right: calc(-1 * var(--book-cover-overhang-right));
           bottom: calc(-1 * var(--book-cover-overhang-bottom)); }
}

/* Close edge keyframes: reverse of open — progressively thicken as flyleaves
   return to the stack. Steps at 31%/51%/71%/94% mirror the open 69%/49%/29%/6%.
   Opacity fades in between 70-94% (mirror of open's 6-30% fade out). */
@keyframes timelineDepthEdgeRightClose {
    0%   { width: var(--book-open-page-depth); opacity: 0; }
    31%  { width: calc(var(--book-closed-page-depth) * 0.76); opacity: 0; }
    51%  { width: calc(var(--book-closed-page-depth) * 0.84); opacity: 0; }
    71%  { width: calc(var(--book-closed-page-depth) * 0.92); opacity: 0; }
    94%  { width: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
    100% { width: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
}

@keyframes timelineDepthEdgeTopClose {
    0%   { left: 50%;  height: var(--book-open-page-depth); opacity: 0; }
    31%  { left: 28%;  height: calc(var(--book-closed-page-depth) * 0.76); opacity: 0; }
    51%  { left: 15%;  height: calc(var(--book-closed-page-depth) * 0.84); opacity: 0; }
    71%  { left: 7%;   height: calc(var(--book-closed-page-depth) * 0.92); opacity: 0; }
    94%  { left: 0;    height: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
    100% { left: 0;    height: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
}

@keyframes timelineDepthEdgeBottomClose {
    0%   { left: 50%;  height: var(--book-open-page-depth); opacity: 0; }
    31%  { left: 28%;  height: calc(var(--book-closed-page-depth) * 0.76); opacity: 0; }
    51%  { left: 15%;  height: calc(var(--book-closed-page-depth) * 0.84); opacity: 0; }
    71%  { left: 7%;   height: calc(var(--book-closed-page-depth) * 0.92); opacity: 0; }
    94%  { left: 0;    height: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
    100% { left: 0;    height: calc(var(--book-closed-page-depth) + var(--book-cover-board-thickness) + 2px); opacity: 1; }
}

/* Depth container close: reverse of open — starts pushed back, returns to 0.
   Front-loaded to 70% to match scrub mode's clamp(0, 1-depth/0.70, 1) curve.
   Children keyframes (back cover, pages, edges) use 70% thresholds that
   assume the container Z is resolved by that point. */
@keyframes timelineDepthFadeClose {
    0%   { opacity: 1; transform: translateZ(calc(-1 * (var(--book-open-page-depth) + 2px))); }
    70%  { opacity: 1; transform: translateZ(0px); }
    100% { opacity: 1; transform: translateZ(0px); }
}

/* Left-stack hide (reverse of reveal) */
@keyframes timelineLeftStackHide {
    0%   { opacity: 1; transform: translateZ(calc(var(--book-open-page-depth) * 0.3)); }
    60%  { opacity: 0; transform: translateZ(calc(var(--book-open-page-depth) * 0.1)); }
    100% { opacity: 0; transform: translateZ(0); }
}

/* ── Close: Frame rotation (open reading tilt → closed angles) ──
   Mirror of frameOpenSettle: starts at the reading angle, reverses
   through the settle stages, then holds at the closed 3D angle.
   Y rotation is kept minimal (<4deg) through 50% while depth elements
   (spine, back cover, page layers) close to their final positions by 70%.
   Full 3D angle reached at 75%, after depth geometry is resolved. */
@keyframes timelineFrameClose {
    /* Start at open-book reading tilt */
    0% {
        transform:
            rotateX(var(--book-open-rotate-x))
            rotateY(var(--book-open-rotate-y))
            rotateZ(var(--book-open-rotate-z));
        box-shadow:
            1px 2px 8px rgba(0,0,0,0.10),
            2px 6px 24px rgba(0,0,0,0.14),
            0 12px 40px -6px rgba(0,0,0,0.10);
    }
    /* Slow lift — Y rotation minimal while depth elements close */
    20% {
        transform: rotateX(5deg) rotateY(1deg) rotateZ(0deg);
        box-shadow:
            1px 3px 10px rgba(0,0,0,0.12),
            3px 7px 26px rgba(0,0,0,0.14),
            0 10px 36px -4px rgba(0,0,0,0.10);
    }
    /* Mid-point: moderate X tilt, minimal Y — depth elements settling */
    50% {
        transform: rotateX(7deg) rotateY(4deg) rotateZ(0deg);
        box-shadow:
            2px 4px 12px rgba(0,0,0,0.16),
            4px 8px 28px rgba(0,0,0,0.13);
    }
    /* Full closed 3D angle — depth geometry resolved by 70% */
    75%, 100% {
        transform:
            rotateX(var(--book-closed-rotate-x))
            rotateY(var(--book-closed-rotate-y))
            rotateZ(var(--book-closed-rotate-z));
        box-shadow:
            4px 6px 15px rgba(0,0,0,0.20),
            8px 12px 35px rgba(0,0,0,0.15);
    }
}

/* ── Close: Info panel hide ── */
@keyframes infoPanelHide {
    0%   { opacity: 1; visibility: visible; }
    100% { opacity: 0; visibility: hidden; }
}

/* Desktop-only close width keyframes */
@media (min-width: 1200px) {
    @keyframes timelineFrameWidthClose {
        0%   { width: min(1600px, 85vw); }
        100% { width: min(800px, 42vw); }
    }

    @keyframes timelineFrameRightClose {
        0%   { width: 50%; margin-left: 50%; }
        100% { width: 100%; margin-left: 0%; }
    }

    /* Desktop back cover close: adds `left` animation synced with frame-right expansion.
       In open state, depth fills full frame but frame-right is only 50% (right half).
       Without this, the back cover spans 100% of frame width while the front cover
       (inside frame-right) spans only 50%, making the back cover appear ~2x wider.
       left: linear 50%→0 across 0-100% — directly synced with frame-right's
       margin-left contraction. Each intermediate step sits exactly on the linear
       path: 20%=40%, 70%=15%. In scrub mode both --timeline-close-depth and
       --timeline-close-width advance together, so no offset is needed. In play mode
       the back cover starts 0.3s before frame-right (delay 1.5s vs 1.8s), creating
       a brief ~10% width excess during the first 20% — invisible because the frame
       is still nearly flat (minimal 3D rotation) at that point. */
    @keyframes timelineDepthBackCoverClose {
        0%   { opacity: 1; transform: translateZ(-1px);
               left: 50%;
               top: calc(-1 * var(--book-cover-overhang-top) - 6px);
               right: calc(-1 * var(--book-cover-overhang-right) - 8px);
               bottom: calc(-1 * var(--book-cover-overhang-bottom) - 6px); }
        20%  { opacity: 1; transform: translateZ(-1px); left: 40%; }
        70%  { opacity: 1; transform: translateZ(-1px); left: 15%; }
        100% { opacity: 1; transform: translateZ(var(--book-cover-board-thickness, 6px));
               left: 0;
               top: calc(-1 * var(--book-cover-overhang-top));
               right: calc(-1 * var(--book-cover-overhang-right));
               bottom: calc(-1 * var(--book-cover-overhang-bottom)); }
    }

    /* Desktop spine close: full copy of timelineDepthSpineClose with `right` animation.
       right: 50% (open center) → 100% (closed left edge), held at 50% through 0-20%,
       then linear to 100% at kf 100%. 70% step: 81.25% = linear at 62.5% of 20→100%. */
    @keyframes timelineDepthSpineCloseDesktop {
        0% {
            opacity: 0;
            right: 50%;
            transform:
                translateZ(calc(var(--book-open-page-depth) + 2px))
                translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
                translateY(var(--book-spine-extra-ty, 0px))
                translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
                rotateX(var(--book-spine-extra-rx, 0deg))
                rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
                rotateZ(var(--book-spine-extra-rz, 0deg));
        }
        15% {
            opacity: 1;
            right: 50%;
            transform:
                translateZ(calc(var(--book-open-page-depth) + 2px))
                translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
                translateY(var(--book-spine-extra-ty, 0px))
                translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
                rotateX(var(--book-spine-extra-rx, 0deg))
                rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
                rotateZ(var(--book-spine-extra-rz, 0deg));
        }
        20% {
            opacity: 1;
            right: 50%;
            transform:
                translateZ(calc(var(--book-open-page-depth) + 2px))
                translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
                translateY(var(--book-spine-extra-ty, 0px))
                translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
                rotateX(var(--book-spine-extra-rx, 0deg))
                rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
                rotateZ(var(--book-spine-extra-rz, 0deg));
        }
        70% {
            opacity: 1;
            right: 81.25%;
            transform:
                translateZ(0px)
                translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
                translateY(var(--book-spine-extra-ty, 0px))
                translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
                rotateX(var(--book-spine-extra-rx, 0deg))
                rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
                rotateZ(var(--book-spine-extra-rz, 0deg));
        }
        100% {
            opacity: 1;
            right: 100%;
            transform:
                translateZ(0px)
                translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
                translateY(var(--book-spine-extra-ty, 0px))
                translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
                rotateX(var(--book-spine-extra-rx, 0deg))
                rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
                rotateZ(var(--book-spine-extra-rz, 0deg));
        }
    }

    /* Desktop back cover close (PLAY MODE ONLY): holds left at 50% through 20%
       to absorb the 0.3s gap between back cover start (delay 0.5) and frame-right
       start (delay 0.6). 20% of 1.5s duration = 0.3s = exactly the gap.
       From 20-100%: left 50%→0% linear, synced with frame-right's margin-left 50%→0%.
       Scrub mode uses the original timelineDepthBackCoverClose (no hold needed
       because --timeline-close-depth and --timeline-close-width advance together).
       70% step: (70-20)/(100-20) = 62.5% → left = 50% * 0.375 = 18.75%.
       Invariant: left 18.75% + spine right 81.25% = 100% at keyframe 70%. */
    @keyframes timelineDepthBackCoverCloseDesktopPlay {
        0%   { opacity: 1; transform: translateZ(-1px);
               left: 50%;
               top: calc(-1 * var(--book-cover-overhang-top) - 6px);
               right: calc(-1 * var(--book-cover-overhang-right) - 8px);
               bottom: calc(-1 * var(--book-cover-overhang-bottom) - 6px); }
        20%  { opacity: 1; transform: translateZ(-1px); left: 50%; }
        70%  { opacity: 1; transform: translateZ(-1px); left: 18.75%; }
        100% { opacity: 1; transform: translateZ(var(--book-cover-board-thickness, 6px));
               left: 0;
               top: calc(-1 * var(--book-cover-overhang-top));
               right: calc(-1 * var(--book-cover-overhang-right));
               bottom: calc(-1 * var(--book-cover-overhang-bottom)); }
    }

    /* Desktop back-edge head/tail close: left from 50% → board-thickness.
       Held at 50% through 0-20%, then linear to bt at 100%.
       70% step: bt + (50% - bt) * 0.375 = bt + 37.5% of range remaining. */
    @keyframes timelineBackEdgeLeftClose {
        0%   { left: 50%; }
        20%  { left: 50%; }
        70%  { left: calc(var(--book-cover-board-thickness, 6px) + (50% - var(--book-cover-board-thickness, 6px)) * 0.375); }
        100% { left: var(--book-cover-board-thickness, 6px); }
    }
}

/* ── Pickup keyframes (transition → keyframe conversion) ──
   Runtime uses CSS transition with cubic-bezier(0.22, 0.68, 0.18, 1).
   Since scrub must use linear timing, we bake the easing into intermediate
   keyframes sampled from the curve. Both start and end use the same
   transform function list so CSS interpolates each property smoothly.

   Sampled cubic-bezier(0.22, 0.68, 0.18, 1) output (via Newton's method):
     x=10% → y=33.2%   x=20% → y=62.2%   x=30% → y=78.7%
     x=50% → y=92.8%   x=70% → y=98.1%   x=85% → y=99.6% */
@keyframes timelinePickupFrame {
    0% {
        transform:
            rotateX(var(--book-table-rotate-x))
            rotateY(var(--book-table-rotate-y))
            rotateZ(var(--book-table-rotate-z))
            scale(var(--book-table-scale))
            translateX(var(--book-table-translate-x))
            translateY(var(--book-table-translate-y));
    }
    /* Rapid initial acceleration — 33% of distance in first 10% of time */
    10% {
        transform:
            rotateX(calc(var(--book-table-rotate-x) + (var(--book-closed-rotate-x) - var(--book-table-rotate-x)) * 0.332))
            rotateY(calc(var(--book-table-rotate-y) + (var(--book-closed-rotate-y) - var(--book-table-rotate-y)) * 0.332))
            rotateZ(calc(var(--book-table-rotate-z) + (var(--book-closed-rotate-z) - var(--book-table-rotate-z)) * 0.332))
            scale(calc(var(--book-table-scale) + (1 - var(--book-table-scale)) * 0.332))
            translateX(calc(var(--book-table-translate-x) * 0.668))
            translateY(calc(var(--book-table-translate-y) * 0.668));
    }
    20% {
        transform:
            rotateX(calc(var(--book-table-rotate-x) + (var(--book-closed-rotate-x) - var(--book-table-rotate-x)) * 0.622))
            rotateY(calc(var(--book-table-rotate-y) + (var(--book-closed-rotate-y) - var(--book-table-rotate-y)) * 0.622))
            rotateZ(calc(var(--book-table-rotate-z) + (var(--book-closed-rotate-z) - var(--book-table-rotate-z)) * 0.622))
            scale(calc(var(--book-table-scale) + (1 - var(--book-table-scale)) * 0.622))
            translateX(calc(var(--book-table-translate-x) * 0.378))
            translateY(calc(var(--book-table-translate-y) * 0.378));
    }
    30% {
        transform:
            rotateX(calc(var(--book-table-rotate-x) + (var(--book-closed-rotate-x) - var(--book-table-rotate-x)) * 0.787))
            rotateY(calc(var(--book-table-rotate-y) + (var(--book-closed-rotate-y) - var(--book-table-rotate-y)) * 0.787))
            rotateZ(calc(var(--book-table-rotate-z) + (var(--book-closed-rotate-z) - var(--book-table-rotate-z)) * 0.787))
            scale(calc(var(--book-table-scale) + (1 - var(--book-table-scale)) * 0.787))
            translateX(calc(var(--book-table-translate-x) * 0.213))
            translateY(calc(var(--book-table-translate-y) * 0.213));
    }
    /* Decelerating through the midpoint */
    50% {
        transform:
            rotateX(calc(var(--book-table-rotate-x) + (var(--book-closed-rotate-x) - var(--book-table-rotate-x)) * 0.928))
            rotateY(calc(var(--book-table-rotate-y) + (var(--book-closed-rotate-y) - var(--book-table-rotate-y)) * 0.928))
            rotateZ(calc(var(--book-table-rotate-z) + (var(--book-closed-rotate-z) - var(--book-table-rotate-z)) * 0.928))
            scale(calc(var(--book-table-scale) + (1 - var(--book-table-scale)) * 0.928))
            translateX(calc(var(--book-table-translate-x) * 0.072))
            translateY(calc(var(--book-table-translate-y) * 0.072));
    }
    /* Gentle settle — nearly at final position */
    70% {
        transform:
            rotateX(calc(var(--book-table-rotate-x) + (var(--book-closed-rotate-x) - var(--book-table-rotate-x)) * 0.981))
            rotateY(calc(var(--book-table-rotate-y) + (var(--book-closed-rotate-y) - var(--book-table-rotate-y)) * 0.981))
            rotateZ(calc(var(--book-table-rotate-z) + (var(--book-closed-rotate-z) - var(--book-table-rotate-z)) * 0.981))
            scale(calc(var(--book-table-scale) + (1 - var(--book-table-scale)) * 0.981))
            translateX(calc(var(--book-table-translate-x) * 0.019))
            translateY(calc(var(--book-table-translate-y) * 0.019));
    }
    100% {
        transform:
            rotateX(var(--book-closed-rotate-x))
            rotateY(var(--book-closed-rotate-y))
            rotateZ(var(--book-closed-rotate-z))
            scale(1)
            translateX(0px)
            translateY(0px);
    }
}

@keyframes timelinePickupGlow {
    0%   { opacity: var(--book-table-glow-intensity); }
    10%  { opacity: calc(var(--book-table-glow-intensity) * 0.668); }
    20%  { opacity: calc(var(--book-table-glow-intensity) * 0.378); }
    30%  { opacity: calc(var(--book-table-glow-intensity) * 0.213); }
    50%  { opacity: calc(var(--book-table-glow-intensity) * 0.072); }
    70%  { opacity: calc(var(--book-table-glow-intensity) * 0.019); }
    100% { opacity: 0; }
}

/* ── Set Down keyframes (reverse of pickup) ──
   Same easing curve sampled in reverse: the book decelerates as it
   settles onto the table (mirror of pickup's acceleration). */
@keyframes timelineSetdownFrame {
    0% {
        transform:
            rotateX(var(--book-closed-rotate-x))
            rotateY(var(--book-closed-rotate-y))
            rotateZ(var(--book-closed-rotate-z))
            scale(1)
            translateX(0px)
            translateY(0px);
    }
    30% {
        transform:
            rotateX(calc(var(--book-closed-rotate-x) + (var(--book-table-rotate-x) - var(--book-closed-rotate-x)) * 0.019))
            rotateY(calc(var(--book-closed-rotate-y) + (var(--book-table-rotate-y) - var(--book-closed-rotate-y)) * 0.019))
            rotateZ(calc(var(--book-closed-rotate-z) + (var(--book-table-rotate-z) - var(--book-closed-rotate-z)) * 0.019))
            scale(calc(1 + (var(--book-table-scale) - 1) * 0.019))
            translateX(calc(var(--book-table-translate-x) * 0.019))
            translateY(calc(var(--book-table-translate-y) * 0.019));
    }
    50% {
        transform:
            rotateX(calc(var(--book-closed-rotate-x) + (var(--book-table-rotate-x) - var(--book-closed-rotate-x)) * 0.072))
            rotateY(calc(var(--book-closed-rotate-y) + (var(--book-table-rotate-y) - var(--book-closed-rotate-y)) * 0.072))
            rotateZ(calc(var(--book-closed-rotate-z) + (var(--book-table-rotate-z) - var(--book-closed-rotate-z)) * 0.072))
            scale(calc(1 + (var(--book-table-scale) - 1) * 0.072))
            translateX(calc(var(--book-table-translate-x) * 0.072))
            translateY(calc(var(--book-table-translate-y) * 0.072));
    }
    70% {
        transform:
            rotateX(calc(var(--book-closed-rotate-x) + (var(--book-table-rotate-x) - var(--book-closed-rotate-x)) * 0.213))
            rotateY(calc(var(--book-closed-rotate-y) + (var(--book-table-rotate-y) - var(--book-closed-rotate-y)) * 0.213))
            rotateZ(calc(var(--book-closed-rotate-z) + (var(--book-table-rotate-z) - var(--book-closed-rotate-z)) * 0.213))
            scale(calc(1 + (var(--book-table-scale) - 1) * 0.213))
            translateX(calc(var(--book-table-translate-x) * 0.213))
            translateY(calc(var(--book-table-translate-y) * 0.213));
    }
    80% {
        transform:
            rotateX(calc(var(--book-closed-rotate-x) + (var(--book-table-rotate-x) - var(--book-closed-rotate-x)) * 0.378))
            rotateY(calc(var(--book-closed-rotate-y) + (var(--book-table-rotate-y) - var(--book-closed-rotate-y)) * 0.378))
            rotateZ(calc(var(--book-closed-rotate-z) + (var(--book-table-rotate-z) - var(--book-closed-rotate-z)) * 0.378))
            scale(calc(1 + (var(--book-table-scale) - 1) * 0.378))
            translateX(calc(var(--book-table-translate-x) * 0.378))
            translateY(calc(var(--book-table-translate-y) * 0.378));
    }
    90% {
        transform:
            rotateX(calc(var(--book-closed-rotate-x) + (var(--book-table-rotate-x) - var(--book-closed-rotate-x)) * 0.668))
            rotateY(calc(var(--book-closed-rotate-y) + (var(--book-table-rotate-y) - var(--book-closed-rotate-y)) * 0.668))
            rotateZ(calc(var(--book-closed-rotate-z) + (var(--book-table-rotate-z) - var(--book-closed-rotate-z)) * 0.668))
            scale(calc(1 + (var(--book-table-scale) - 1) * 0.668))
            translateX(calc(var(--book-table-translate-x) * 0.668))
            translateY(calc(var(--book-table-translate-y) * 0.668));
    }
    100% {
        transform:
            rotateX(var(--book-table-rotate-x))
            rotateY(var(--book-table-rotate-y))
            rotateZ(var(--book-table-rotate-z))
            scale(var(--book-table-scale))
            translateX(var(--book-table-translate-x))
            translateY(var(--book-table-translate-y));
    }
}

@keyframes timelineSetdownGlow {
    0%   { opacity: 0; }
    30%  { opacity: calc(var(--book-table-glow-intensity) * 0.019); }
    50%  { opacity: calc(var(--book-table-glow-intensity) * 0.072); }
    70%  { opacity: calc(var(--book-table-glow-intensity) * 0.213); }
    80%  { opacity: calc(var(--book-table-glow-intensity) * 0.378); }
    90%  { opacity: calc(var(--book-table-glow-intensity) * 0.668); }
    100% { opacity: var(--book-table-glow-intensity); }
}

/* ═══════════════════════════════════════════════════════════════
   SHARED SCRUB STRUCTURAL OVERRIDES
   Applied when .book-desk-scene--scrub is active (any mode).
   ═══════════════════════════════════════════════════════════════ */

.book-desk-scene--scrub .book-frame-right {
    width: 100%;
    flex: 1;
    transform-style: preserve-3d;
    overflow: visible;
    z-index: auto; /* Reset desktop z-index:2 — prevents stacking context from
                      occluding .book-3d-depth siblings (spine, edges) in preserve-3d */
}

.book-desk-scene--scrub .book-3d-depth {
    transition: none !important; /* Kill base safety transition (desk-scene.css) —
                                    prevents transient opacity/visibility fade during
                                    state-class removal when entering scrub mode */
}

.book-desk-scene--scrub .book-viewport,
.book-desk-scene--scrub .book-page-stack {
    transform-style: preserve-3d;
}

.book-desk-scene--scrub .book-cover-page {
    transform-origin: left center;
}

.book-desk-scene--scrub .book-viewport {
    perspective: none;
    background: transparent;
}

.book-desk-scene--scrub .book-nav,
.book-desk-scene--scrub .book-nav-ribbons {
    opacity: 0 !important;
    pointer-events: none !important;
}

/* Open scrub: nav and ribbons visible — attached to pages, occluded by cover/flyleaf z-index */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-nav,
.book-desk-scene--scrub[data-scrub-mode="open"] .book-nav-ribbons {
    opacity: 1 !important;
    pointer-events: auto !important;
}
.book-desk-scene--scrub[data-scrub-mode="open"] .book-page-sticky-notes,
.book-desk-scene--scrub[data-scrub-mode="open"] .book-page-ribbon-container,
.book-desk-scene--scrub[data-scrub-mode="open"] .book-page-ribbon-container-back {
    opacity: 1 !important;
    pointer-events: auto !important;
}
/* Counteract each page's translateZ(--page-z-offset * -0.5px) and push ribbons
   IN FRONT of the hardcover-top-clip (Z=1px) so ribbon tips are visible and
   clickable above the leather edge, while the clip still covers their bases. */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-page-ribbon-container {
    transform: translateZ(calc(var(--page-z-offset, 0) * 0.5px + 2px));
}

.book-desk-scene--scrub .book-frame:hover {
    transform: unset;
}
.book-desk-scene--scrub .book-frame.book-frame--no-hover:hover {
    transform: unset;
}

/* Default: hide decorative elements that may interfere with
   scrub animations. Mode-specific overrides below restore
   visibility where these elements would appear at runtime. */
.book-desk-scene--scrub .book-spine-3d {
    opacity: 0 !important;
    visibility: hidden !important;
}
.book-desk-scene--scrub .book-frame::after {
    opacity: 0;
    visibility: hidden;
}
/* Pickup/SetDown: the book is closed throughout, so spine, frame shadow,
   and closed-book ribbons are visible (matches Cover state). */
.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-spine-3d,
.book-desk-scene--scrub[data-scrub-mode="setdown"] .book-spine-3d {
    opacity: 1 !important;
    visibility: visible !important;
}
.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-frame::after,
.book-desk-scene--scrub[data-scrub-mode="setdown"] .book-frame::after {
    opacity: 1;
    visibility: visible;
}
.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-3d-ribbons,
.book-desk-scene--scrub[data-scrub-mode="setdown"] .book-3d-ribbons {
    opacity: 1;
}

/* Open scrub: the flat 2D spine stays hidden — the true 3D spine inside
   .book-3d-depth handles all spine rendering during the opening animation.
   The 3D spine's opacity is controlled by the timelineDepthSpine keyframe. */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-spine-3d {
    opacity: 0 !important;
    visibility: hidden !important;
}
.book-desk-scene--scrub[data-scrub-mode="open"] .book-frame::after {
    opacity: 1;
    visibility: visible;
}
/* Back cover edges stay visible during scrub */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-back-edge {
    opacity: 1 !important;
    visibility: visible !important;
}
/* Ribbons fade out as depth progresses — gone by 30% of depth track.
   Matches runtime behavior where ribbons fade with transition: opacity 0.5s ease 0.8s. */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-ribbons {
    animation: timelineRibbonFade 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-depth)) !important;
    transition: none !important;
}

/* Close scrub: the flat 2D spine stays hidden — the true 3D spine inside
   .book-3d-depth handles all spine rendering during the close animation.
   The 3D spine's opacity is controlled by the timelineDepthSpineClose keyframe. */
.book-desk-scene--scrub[data-scrub-mode="close"] .book-spine-3d {
    opacity: 0 !important;
    visibility: hidden !important;
}
.book-desk-scene--scrub[data-scrub-mode="close"] .book-frame::after {
    opacity: 1;
    visibility: visible;
}
/* Ribbons reappear as book closes — visible again at 70% of close-depth track */
.book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-ribbons {
    animation: timelineRibbonReveal 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-depth)) !important;
    transition: none !important;
}

/* ═══════════════════════════════════════════════════════════════
   SCRUB MODE: OPEN (data-scrub-mode="open")
   Existing behavior — cover swing, frame, depth, info tracks
   ═══════════════════════════════════════════════════════════════ */

.book-desk-scene--scrub[data-scrub-mode="open"] .book-desk-surface {
    perspective: var(--book-closed-perspective);
    transform-style: preserve-3d;
    pointer-events: none; /* Let ribbon clicks pass through (matches Book state fix) */
}

/* Track 1: Cover swing */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-cover-page {
    animation: coverPageOpen 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-cover)) !important;
    transition: none !important;
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    z-index: 100 !important;
    /* Hardcover overhang — cover boards extend beyond the page block,
       matching the real Opening state (desk-scene.css:2671-2677). */
    top: calc(-1 * var(--book-cover-overhang-top));
    left: 0;
    right: calc(-1 * var(--book-cover-overhang-right));
    bottom: calc(-1 * var(--book-cover-overhang-bottom));
}

/* Track 2+3: Frame rotation + width */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-frame {
    animation: frameOpenSettle 1s linear paused both,
               timelineFrameWidthDefault 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-frame)),
                     calc(-1s * var(--timeline-width)) !important;
    transition: none !important;
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
}

/* Track 4: Depth container — visible, 3D context preserved.
   CRITICAL: Do NOT use CSS animation here. Chromium flattens children's
   3D transforms when a paused animation sets `transform` on a preserve-3d
   container, making rotated elements (spine rotateY -90deg, edges rotateX
   +-90deg) project to zero-width/height — invisible. Instead, use calc()
   interpolation via the --timeline-depth variable (0→1, set by JS scrub). */
/* Delay Z-push until 30% of depth so the container stays flush during the
   early cover swing. Before 30%: Z=0 (closed position). After 30%: ramps
   to full backward push. Synced with the 30% hold on depth children and
   the frame width expansion (master ~35%).
   clamp() maps depth 0.30→1.0 to a 0→1 factor for the Z displacement. */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-depth {
    transform: translateZ(calc(-1 * clamp(0, (var(--timeline-depth) - 0.30) / 0.70, 1) * (var(--book-open-page-depth) + 2px)));
    transition: none !important;
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: none;
    transform-style: preserve-3d;
}

/* Track 4: Depth children geometry — closed → open positions.
   All driven by --timeline-depth (master 10-50%), same as the container.
   Every child gets opacity: 1 + visibility: visible to prevent any
   inherited or cascading rule from hiding depth geometry during scrub. */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-page {
    animation: timelineDepthPage 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-depth)) !important;
    transition: none !important;
    /* opacity controlled by keyframe (0→1 during 30-45% of depth track)
       to hide pages during the cover-swing phase where perspective
       projection causes higher-Z layers to clip above the frame. */
    visibility: visible !important;
}
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-spine {
    /* NO CSS animation — Chromium flattens preserve-3d children when a paused
       animation is applied to their parent. The spine has preserve-3d for its
       headcap/tailcap/edge-wall children (strips 0-2 with rotateX/rotateY).
       Using calc() interpolation on opacity instead of keyframe animation
       preserves the 3D rendering of these perpendicular surfaces. */
    transition: none !important;
    /* Visible at depth 0-85%, fades to 0 at 85-100% (viewport covers spine by then) */
    opacity: calc(1 - clamp(0, (var(--timeline-depth) - 0.85) / 0.15, 1)) !important;
    visibility: visible !important;
    /* Curvature holds at closed (20deg) until 30% of depth, then ramps to open (130deg).
       Synced with the 30% hold on depth keyframes and frame expansion.
       clamp() maps depth 0.30→1.0 to 0→1. */
    --book-spine-curvature: calc(20deg + clamp(0, (var(--timeline-depth) - 0.30) / 0.70, 1) * 110deg);
    /* Z-compensation: counteracts the depth container's backward push.
       At depth<0.30 the container stays at Z=0, so the spine needs no offset.
       From 30-100% the container ramps to translateZ(-(open-page-depth+2)),
       so the spine ramps to translateZ(+(open-page-depth+2)) to stay at Z=0.
       Includes -board-thickness offset matching the base CSS rule so the spine
       starts at Z=-bt (aligned with back cover outer face) at depth 0%. */
    transform:
        translateZ(calc(
            clamp(0, (var(--timeline-depth) - 0.30) / 0.70, 1)
            * (var(--book-open-page-depth) + 2px)
        ))
        translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
        translateY(var(--book-spine-extra-ty, 0px))
        translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
        rotateX(var(--book-spine-extra-rx, 0deg))
        rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
        rotateZ(var(--book-spine-extra-rz, 0deg));
}
/* Back cover container stays visible (edge children need it) */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-back-cover {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
}
/* Back cover face stays visible throughout — transform transitions from
   board-thickness Z (closed) to -1px (open) via timelineDepthBackCover. */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-back-cover-face {
    animation: timelineDepthBackCover 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-depth)) !important;
    transition: none !important;
    visibility: visible !important;
}
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-edge-right {
    animation: timelineDepthEdgeRight 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-depth)) !important;
    transition: none !important;
    /* opacity controlled by keyframe (1→0 during 6-30% of depth track) */
    visibility: visible !important;
}
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-edge-top {
    animation: timelineDepthEdgeTop 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-depth)) !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-edge-bottom {
    animation: timelineDepthEdgeBottom 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-depth)) !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}

/* Desktop: use actual spread width keyframes + animate frame-right */
@media (min-width: 1200px) {
    .book-desk-scene--scrub[data-scrub-mode="open"] .book-frame {
        animation-name: frameOpenSettle, timelineFrameWidthDesktop !important;
        height: min(88vh, 800px);
    }

    /* Frame-right: width 100% → 50%, anchored right via margin-left:auto.
       Since info panel is absolute, frame-right is the only flex child.
       At 0% (closed): 100% of 800px = 800px (full frame).
       At 100% (open): 50% of 1600px = 800px (right half). */
    .book-desk-scene--scrub[data-scrub-mode="open"] .book-frame-right {
        animation: timelineFrameRightWidth 1s linear paused both !important;
        animation-delay: calc(-1s * var(--timeline-width)) !important;
        flex: none !important;
        /* margin-left animated via timelineFrameRightWidth keyframe (0% → 50%) */
        transition: none !important;
    }
}

/* Pages visible from start in scrub — same z-index stacking as play mode */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-page {
    opacity: 1 !important;
    visibility: visible !important;
    z-index: 50 !important;
    pointer-events: none;
}

/* Welcome content is now static (opacity: 1 by default) — no scrub override needed */

/* Theme ribbon toggle — visible from start */
.book-desk-scene--scrub[data-scrub-mode="open"] .theme-ribbon {
    opacity: 1 !important;
    visibility: visible !important;
}

/* Flyleaf cascade during open scrub — staggered per-leaf tracks.
   In runtime, flyleaves cascade simultaneously with the cover swing at
   staggered delays (0.64x, 0.82x, 1.0x, 1.18x of cover duration).
   Each leaf is driven by its own --timeline-open-flyN variable.
   Reuses the existing flyleafTurnOpen keyframe from storybook-unified.css.
   The standalone Flyleaf mode (data-scrub-mode="flyleaf") is kept for
   isolated debugging of individual leaf behavior. */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-flyleaf {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
}

.book-desk-scene--scrub[data-scrub-mode="open"] .book-flyleaf--1 {
    animation: flyleafTurnOpen 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-open-fly1)) !important;
    z-index: 90 !important;
}
.book-desk-scene--scrub[data-scrub-mode="open"] .book-flyleaf--2 {
    animation: flyleafTurnOpen 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-open-fly2)) !important;
    z-index: 82 !important;
}
.book-desk-scene--scrub[data-scrub-mode="open"] .book-flyleaf--3 {
    animation: flyleafTurnOpen 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-open-fly3)) !important;
    z-index: 74 !important;
}
.book-desk-scene--scrub[data-scrub-mode="open"] .book-flyleaf--4 {
    animation: flyleafTurnOpen 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-open-fly4)) !important;
    z-index: 66 !important;
}

/* Flyleaf 4 back content during scrub/play — pointer-events disabled
   so ToC buttons don't interfere with the animation scrub/playback.
   Content stays visible because the hide rule targets the scene state
   class (--book/--settling) which is suppressed during scrub/play. */
.book-desk-scene--scrub .book-flyleaf-back-content,
.book-desk-scene--play .book-flyleaf-back-content {
    pointer-events: none;
}

/* Left-stack animated reveal — appears as cover opens and depth geometry
   transitions. The timelineLeftStackReveal keyframe holds opacity:0 for the
   first 40% then fades in, matching the cover-swing phase where turned pages
   don't exist yet. Driven by --timeline-depth. */
.book-desk-scene--scrub[data-scrub-mode="open"] .book-3d-left-stack {
    animation: timelineLeftStackReveal 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-depth)) !important;
    transition: none !important;
    pointer-events: none;
}

/* ═══════════════════════════════════════════════════════════════
   SCRUB MODE: CLOSE (data-scrub-mode="close")
   Full close: flyleaf cascade + cover close + frame rotation +
   frame width contraction + info panel hide + depth geometry.
   ═══════════════════════════════════════════════════════════════ */

.book-desk-scene--scrub[data-scrub-mode="close"] .book-desk-surface {
    perspective: var(--book-closed-perspective);
    transform-style: preserve-3d;
}

/* Frame: animated rotation flat → closed 3D angle via --timeline-close-frame.
   No perspective() in transform — parent provides it via CSS property. */
.book-desk-scene--scrub[data-scrub-mode="close"] .book-frame {
    animation: timelineFrameClose 1s linear paused both,
               timelineFrameWidthDefault 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-frame)),
                     calc(-1s * var(--timeline-close-width)) !important;
    transition: none !important;
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
}

/* Cover close track — with overhang matching open scrub pattern */
.book-desk-scene--scrub[data-scrub-mode="close"] .book-cover-page {
    animation: coverPageClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-cover)) !important;
    transition: none !important;
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    top: calc(-1 * var(--book-cover-overhang-top));
    left: 0;
    right: calc(-1 * var(--book-cover-overhang-right));
    bottom: calc(-1 * var(--book-cover-overhang-bottom));
}

/* Flyleaf tracks — each independently scrubbable */
.book-desk-scene--scrub[data-scrub-mode="close"] .book-flyleaf {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
}

.book-desk-scene--scrub[data-scrub-mode="close"] .book-flyleaf--4 {
    animation: flyleafTurnClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-fly4)) !important;
}
.book-desk-scene--scrub[data-scrub-mode="close"] .book-flyleaf--3 {
    animation: flyleafTurnClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-fly3)) !important;
}
.book-desk-scene--scrub[data-scrub-mode="close"] .book-flyleaf--2 {
    animation: flyleafTurnClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-fly2)) !important;
}
.book-desk-scene--scrub[data-scrub-mode="close"] .book-flyleaf--1 {
    animation: flyleafTurnClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-fly1)) !important;
}

/* Depth container: visible with 3D context. translateZ transitions from
   pushed-back (open position) to flush (closed position) via calc().
   Same preserve-3d protection as open scrub — no CSS animation allowed. */
/* Close Z-push reverses: starts fully pushed back, returns to Z=0 as cover closes.
   Geometry holds at open positions for first 70%, then transitions back to closed.
   Reverse of the open 30% hold — depth snaps back in the final third. */
.book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-depth {
    transform: translateZ(calc(-1 * clamp(0, 1 - var(--timeline-close-depth) / 0.70, 1) * (var(--book-open-page-depth) + 2px)));
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: none;
    transform-style: preserve-3d;
    transition: none !important;
}

/* Depth children: open → closed geometry via --timeline-close-depth.
   Same visibility enforcement as open mode. */
.book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-page {
    animation: timelineDepthPageClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-depth)) !important;
    transition: none !important;
    /* opacity controlled by keyframe — pages fade out as they return to
       closed Z-spread to prevent perspective overflow clipping */
    visibility: visible !important;
}
.book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-spine {
    /* NO CSS animation — same Chromium preserve-3d flattening issue as open scrub.
       calc() interpolation preserves 3D rendering of headcap/wall children. */
    transition: none !important;
    /* Invisible at depth 0% (fully open), fades in at 0-15% */
    opacity: calc(clamp(0, var(--timeline-close-depth) / 0.15, 1)) !important;
    visibility: visible !important;
    /* Curvature: stays at 130deg for first 70% of close, then drops to 20deg.
       Reverse of open's 30% hold: geometry only changes in the last 30% of close. */
    --book-spine-curvature: calc(20deg + clamp(0, 1 - var(--timeline-close-depth) / 0.70, 1) * 110deg);
    /* Z-compensation matches the delayed close Z-push above.
       Includes -board-thickness offset matching the base CSS rule. */
    transform:
        translateZ(calc(clamp(0, 1 - var(--timeline-close-depth) / 0.70, 1) * (var(--book-open-page-depth) + 2px)))
        translateX(calc(var(--book-spine-offset-x, 0px) + var(--book-spine-extra-tx, 0px)))
        translateY(var(--book-spine-extra-ty, 0px))
        translateZ(calc(-1 * var(--book-cover-board-thickness) + var(--book-spine-extra-tz, 0px)))
        rotateX(var(--book-spine-extra-rx, 0deg))
        rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
        rotateZ(var(--book-spine-extra-rz, 0deg));
}
.book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-back-cover {
    animation: timelineDepthBackCoverClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-depth)) !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}
.book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-edge-right {
    animation: timelineDepthEdgeRightClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-depth)) !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}
.book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-edge-top {
    animation: timelineDepthEdgeTopClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-depth)) !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}
.book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-edge-bottom {
    animation: timelineDepthEdgeBottomClose 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-depth)) !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}

/* Left-stack: visible → hidden during close */
.book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-left-stack {
    animation: timelineLeftStackHide 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-close-depth)) !important;
    transition: none !important;
    pointer-events: none;
}

/* Pages hidden during close scrub */
.book-desk-scene--scrub[data-scrub-mode="close"] .book-page {
    opacity: 0 !important;
    visibility: hidden !important;
}

/* Desktop: actual frame width contraction + frame-right + info panel positioning */
@media (min-width: 1200px) {
    .book-desk-scene--scrub[data-scrub-mode="close"] .book-frame {
        animation-name: timelineFrameClose, timelineFrameWidthClose !important;
        height: min(88vh, 800px);
    }

    .book-desk-scene--scrub[data-scrub-mode="close"] .book-frame-right {
        animation: timelineFrameRightClose 1s linear paused both !important;
        animation-delay: calc(-1s * var(--timeline-close-width)) !important;
        flex: none !important;
        transition: none !important;
    }

    /* Desktop spine close: animate `right` from 50% (open center) to 100% (closed left edge).
       Directly synced with frame-right expansion — both --timeline-close-depth and
       --timeline-close-width share the same scrub range (40-76), so no offset needed.
       At depth=0: right=50% (center). At depth=1: right=100% (left edge). Linear between.
       NOTE: Play-mode keyframe has a separate 20% hold because the spine animation starts
       0.3s before frame-right (different delays), but in scrub mode both advance together. */
    .book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-spine {
        right: calc(100% - 50% * clamp(0, 1 - var(--timeline-close-depth), 1)) !important;
    }

    /* Desktop back-edge head/tail close: animate `left` from 50% (open right-half) to
       board-thickness (closed full-width). Same direct sync as spine — no offset. */
    .book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-back-edge--head,
    .book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-back-edge--tail {
        left: calc(var(--book-cover-board-thickness, 6px) + (50% - var(--book-cover-board-thickness, 6px))
              * clamp(0, 1 - var(--timeline-close-depth), 1)) !important;
    }

    /* Desktop close: hide edge-top, edge-bottom, and left-stack to prevent 3D
       compositing ghosting. These elements are positioned for the full-width frame
       but the visible area is only the right half. Edges are at opacity:0 in their
       keyframe until 94%, and left-stack fades by 60%, but visibility:visible on the
       scrub rule keeps them in the preserve-3d composite, causing ghost artifacts.
       After scrub mode ends, base CSS handles their final closed-book appearance. */
    .book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-edge-top,
    .book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-edge-bottom,
    .book-desk-scene--scrub[data-scrub-mode="close"] .book-3d-left-stack {
        opacity: 0 !important;
        visibility: hidden !important;
    }
}

/* ═══════════════════════════════════════════════════════════════
   SCRUB MODE: FLYLEAF (data-scrub-mode="flyleaf")
   Flyleaf open cascade (1→2→3→4) — cover shown turned as context
   ═══════════════════════════════════════════════════════════════ */

.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-desk-surface {
    perspective: var(--book-closed-perspective);
    transform-style: preserve-3d;
}

.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-frame {
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
    transition: none !important;
    transform: perspective(var(--book-closed-perspective))
               rotateX(0deg) rotateY(0deg) rotateZ(0deg);
}

/* Cover shown turned (open) as context */
.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-cover-page {
    transform: translateZ(0px) rotateY(-180deg) !important;
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
}

/* Flyleaf tracks */
.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-flyleaf {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
}

.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-flyleaf--1 {
    animation: flyleafTurnOpen 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fly1)) !important;
}
.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-flyleaf--2 {
    animation: flyleafTurnOpen 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fly2)) !important;
}
.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-flyleaf--3 {
    animation: flyleafTurnOpen 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fly3)) !important;
}
.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-flyleaf--4 {
    animation: flyleafTurnOpen 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fly4)) !important;
}

.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-3d-depth {
    opacity: 0 !important;
    pointer-events: none;
}
.book-desk-scene--scrub[data-scrub-mode="flyleaf"] .book-page {
    opacity: 0;
    visibility: hidden;
}

/* ═══════════════════════════════════════════════════════════════
   SCRUB MODE: PICKUP (data-scrub-mode="pickup")
   Table → Cover: frame transform + glow fade
   ═══════════════════════════════════════════════════════════════ */

.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-desk-surface {
    perspective: var(--timeline-active-perspective, var(--book-table-perspective));
    transform-style: preserve-3d;
}

.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-frame {
    animation: timelinePickupFrame 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-pickup-frame)) !important;
    transition: none !important;
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
}

.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-cover-page {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
}

/* Cover at front (closed position) during pickup */
.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-cover-page {
    transform:
        translateX(var(--book-front-cover-extra-tx))
        translateY(var(--book-front-cover-extra-ty))
        translateZ(calc(var(--book-closed-page-depth) + 2px + var(--book-front-cover-extra-tz)))
        rotateX(var(--book-front-cover-extra-rx))
        rotateY(var(--book-front-cover-extra-ry))
        rotateZ(var(--book-front-cover-extra-rz)) !important;
}

/* 3D depth visible during pickup (closed book) */
.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-3d-depth {
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: none;
    transform-style: preserve-3d;
    transition: none !important;
}

/* Glow fade track */
.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-3d-depth::before {
    animation: timelinePickupGlow 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-pickup-glow)) !important;
}

.book-desk-scene--scrub[data-scrub-mode="pickup"] .book-page {
    opacity: 0;
    visibility: hidden;
}

/* ═══════════════════════════════════════════════════════════════
   SCRUB MODE: SET DOWN (data-scrub-mode="setdown")
   Cover → Table: reverse frame transform + glow fade in
   ═══════════════════════════════════════════════════════════════ */

.book-desk-scene--scrub[data-scrub-mode="setdown"] .book-desk-surface {
    perspective: var(--timeline-active-perspective, var(--book-closed-perspective));
    transform-style: preserve-3d;
}

.book-desk-scene--scrub[data-scrub-mode="setdown"] .book-frame {
    animation: timelineSetdownFrame 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-setdown-frame)) !important;
    transition: none !important;
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
}

.book-desk-scene--scrub[data-scrub-mode="setdown"] .book-cover-page {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
    transform:
        translateX(var(--book-front-cover-extra-tx))
        translateY(var(--book-front-cover-extra-ty))
        translateZ(calc(var(--book-closed-page-depth) + 2px + var(--book-front-cover-extra-tz)))
        rotateX(var(--book-front-cover-extra-rx))
        rotateY(var(--book-front-cover-extra-ry))
        rotateZ(var(--book-front-cover-extra-rz)) !important;
}

.book-desk-scene--scrub[data-scrub-mode="setdown"] .book-3d-depth {
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: none;
    transform-style: preserve-3d;
    transition: none !important;
}

.book-desk-scene--scrub[data-scrub-mode="setdown"] .book-3d-depth::before {
    animation: timelineSetdownGlow 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-setdown-glow)) !important;
}

.book-desk-scene--scrub[data-scrub-mode="setdown"] .book-page {
    opacity: 0;
    visibility: hidden;
}

/* ═══════════════════════════════════════════════════════════════
   SCRUB MODE: PAGE TURN (data-scrub-mode="pageturn")
   Uses scrubPageTurn() JS — no CSS animation rules needed here.
   The page-turn scrub mechanism is in book-system.css (.book-page--scrubbing).
   ═══════════════════════════════════════════════════════════════ */

/* Structural: keep 3D context for page turn preview */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-desk-surface {
    perspective: var(--book-closed-perspective);
    transform-style: preserve-3d;
}

.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-frame {
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
    transition: none !important;
    transform: perspective(var(--book-closed-perspective))
               rotateX(0deg) rotateY(0deg) rotateZ(0deg);
}

/* Show pages for page turn preview (book is open).
   Individual pages can be hidden by JS via .book-page--scrub-hidden
   to prevent Z-buffer ghosting (mirrors runtime turnToPage behaviour). */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page {
    opacity: 1 !important;
    visibility: visible !important;
}

/* Non-participating pages hidden during page turn scrub.
   Only the content faces (front/back/flap/particles) are hidden — ribbon
   containers are siblings and stay visible so all 6 ribbons are shown
   at the top of the book throughout the page turn scrub. */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page.book-page--scrub-hidden {
    pointer-events: none;
}
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page.book-page--scrub-hidden > .book-page-front,
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page.book-page--scrub-hidden > .book-page-back,
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page.book-page--scrub-hidden > .book-page-drag-flap,
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page.book-page--scrub-hidden > .book-page-particles,
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page.book-page--scrub-hidden > .book-page-spine-glow,
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page.book-page--scrub-hidden > .book-page-edge {
    visibility: hidden !important;
    opacity: 0 !important;
}

/* Turned pages' front ribbons: override desk-scene.css display:none
   so all ribbons are visible during page turn scrub (matches Book state). */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page--turned .book-page-ribbon-container {
    display: block !important;
}

/* Destination page — the page behind the turning page that becomes
   the right side of the next spread. Override the book-system.css
   :has(.book-page--turning) pushback (-10px) that would otherwise
   push this page behind the hardcover (-5px). Restore normal depth
   so the destination content is visible during page turn scrub. */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page--scrub-dest {
    transform: translateZ(calc(var(--page-z-offset, 0) * -0.5px)) rotateY(var(--book-page-rest-angle, 0deg)) !important;
}

/* Cover shown turned as context (book open state).
   z-index: 0 overrides Blazor inline z-index (100 in Cover state) so
   cover doesn't occlude content pages (z-index 1+ from scrubPageTurn JS).
   translateZ(-10px) matches the :has(.book-page--turning) push-back depth
   so z-index (not 3D depth) determines stacking — without this, the cover
   at Z=0 sits physically in front of content pages pushed to Z=-10px. */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-cover-page {
    transform: translateZ(-10px) rotateY(-180deg) !important;
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
    z-index: 0 !important;
}

/* Flyleaves turned as context (book open state).
   z-index: 0 overrides Blazor inline z-index (66-90 in Cover state) so
   flyleaf back faces don't occlude turned content pages.
   translateZ(-10px) matches turned content pages' depth so z-index resolves
   stacking — without this, flyleaves at Z=2px sit 12px in front of turned
   pages at Z=-10px, causing Table of Contents bleed-through. */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-flyleaf {
    transform: translateZ(-10px) rotateY(-180deg) !important;
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
    z-index: 0 !important;
}

/* Hide flyleaf back-face content (Table of Contents) during page turn scrub.
   In runtime Book state, .book-desk-scene--book hides it via opacity:0
   (storybook-unified.css line 255), but during scrub the state class is
   suppressed. Without this rule, the ToC bleeds through turned content
   pages on the left side of the spread. */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-flyleaf-back-content {
    opacity: 0 !important;
    pointer-events: none;
}

.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-3d-depth {
    opacity: 0 !important;
    pointer-events: none;
}

/* Page turn preview: nav ribbons visible (book is open, overrides blanket scrub hide) */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-nav,
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-nav-ribbons {
    opacity: 1 !important;
    pointer-events: auto !important;
}
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page-sticky-notes,
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page-ribbon-container,
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .book-page-ribbon-container-back {
    opacity: 1 !important;
    pointer-events: auto !important;
}

/* Hardcover removed — 3D back cover provides the leather border.
   Frame-level .book-3d-back-edge strips provide perpendicular borders. */

/* Welcome content is now static (opacity: 1 by default) — no pageturn override needed */

/* Theme ribbon toggle visible during page turn */
.book-desk-scene--scrub[data-scrub-mode="pageturn"] .theme-ribbon {
    opacity: 1 !important;
    visibility: visible !important;
}

/* ═══════════════════════════════════════════════════════════════
   TOGGLEABLE EFFECT PREVIEWS (data-scrub-fx attribute)
   Each effect is activated by a space-separated token in the
   data-scrub-fx attribute, set by editorInterop.setScrubEffects().
   Effects are driven by --timeline-fx-* CSS variables injected
   by AnimationTesterPanel.ApplyScrub() when the toggle is active.
   Reuses the runtime @keyframes from storybook-unified.css and
   desk-scene.css — no duplicate keyframe definitions.
   ═══════════════════════════════════════════════════════════════ */

/* ── Effect: Spine Glow ──
   Runtime: coverOpenSpineGlow 1.6s on .book-spine-3d-glow (storybook-unified.css)
   Track: --timeline-fx-glow, master 0-50% */

.book-desk-scene--scrub[data-scrub-fx~="glow"] .book-spine-3d {
    opacity: 1 !important;
    visibility: visible !important;
}

.book-desk-scene--scrub[data-scrub-fx~="glow"] .book-spine-3d-glow {
    animation: coverOpenSpineGlow 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fx-glow, 0)) !important;
}

/* ── Effect: Frame Ambient Glow ──
   Runtime: bookOpenAmbient 2s on .book-frame (storybook-unified.css)
   Uses ::before pseudo to avoid box-shadow conflict with frame rotation.
   Track: --timeline-fx-ambient, master 0-63% */

.book-desk-scene--scrub[data-scrub-fx~="ambient"] .book-frame::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    z-index: -1;
    animation: bookOpenAmbient 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fx-ambient, 0)) !important;
}

/* ── Effect: Particle Burst (16 Motes) ──
   Runtime: openBurstMote 1.6s on .book-open-mote (storybook-unified.css)
   Single track drives all 16 motes uniformly. Per-mote --mote-dx/dy
   custom properties from their existing definitions give each mote a
   unique trajectory. Track: --timeline-fx-motes, master 9-59% */

.book-desk-scene--scrub[data-scrub-fx~="motes"] .book-open-mote {
    animation: openBurstMote 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fx-motes, 0)) !important;
}

/* ── Effect: Gutter Shadow ──
   Runtime: gutterShadowReveal 0.5s delay 0.9s on .book-viewport::before (desk-scene.css)
   Track: --timeline-fx-gutter, master 28-44% */

.book-desk-scene--scrub[data-scrub-fx~="gutter"] .book-viewport::before {
    content: '';
    position: absolute;
    left: 0;
    top: 3%;
    bottom: 3%;
    width: clamp(8px, 1.5vw, 18px);
    background: linear-gradient(to right,
        rgba(0,0,0,0.10) 0%,
        rgba(0,0,0,0.04) 40%,
        transparent 100%);
    z-index: 10;
    pointer-events: none;
    animation: gutterShadowReveal 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fx-gutter, 0)) !important;
}

/* ── Effect: Viewport Cream Reveal ──
   Runtime: viewportCreamReveal 0.5s delay 0.7s on .book-viewport (desk-scene.css)
   Track: --timeline-fx-cream, master 22-38% */

.book-desk-scene--scrub[data-scrub-fx~="cream"] .book-viewport {
    animation: viewportCreamReveal 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fx-cream, 0)) !important;
}

/* ── Effect: Breathe Settle ──
   Runtime: bookSettleBreathe 1.8s delay 0.4s on .book-frame in Settling state
   Uses ::after pseudo (ground shadow is already hidden in scrub, so repurposed).
   Track: --timeline-fx-breathe, master 87-100% */

.book-desk-scene--scrub[data-scrub-fx~="breathe"] .book-frame::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    z-index: -1;
    opacity: 1 !important;
    visibility: visible !important;
    animation: bookSettleBreathe 1s linear paused both !important;
    animation-delay: calc(-1s * var(--timeline-fx-breathe, 0)) !important;
}

/* ═══════════════════════════════════════════════════════════════
   PLAY MODE — running animations with configurable speed
   data-play-mode attribute selects which animation plays.

   Triggered by AnimationTesterPanel.HandlePlay() via
   editorInterop.setPlayMode(). Uses linear easing to match
   scrub mode exactly — what you polish in scrub is what plays back.
   --timeline-play-duration is set by C# before entering play mode,
   computed as baseDuration / animationSpeed.
   ═══════════════════════════════════════════════════════════════ */

/* ── Play: Open ──
   Staggered timing matches the scrub track defaults:
   Cover 0-50%, Frame Rotate 35-85%, Frame Width 50-95%, Info 75-100%.
   Each animation's duration = (end-start)% of total, delay = start% of total. */
.book-desk-scene--play[data-play-mode="open"] .book-cover-page {
    animation: coverPageOpen calc(var(--timeline-play-duration, 1.4s) * 0.5)
               linear both !important;
    animation-play-state: running !important;
    transition: none !important;
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    z-index: 100 !important;
    /* Hardcover overhang */
    top: calc(-1 * var(--book-cover-overhang-top));
    left: 0;
    right: calc(-1 * var(--book-cover-overhang-right));
    bottom: calc(-1 * var(--book-cover-overhang-bottom));
}

/* Z-index enforcement — overrides C# inline styles which change based on _state.
   During play mode, the closed-book stacking must be maintained regardless of state. */
.book-desk-scene--play[data-play-mode="open"] .book-flyleaf--1 {
    z-index: 90 !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-flyleaf--2 {
    z-index: 82 !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-flyleaf--3 {
    z-index: 74 !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-flyleaf--4 {
    z-index: 66 !important;
}

.book-desk-scene--play[data-play-mode="open"] .book-desk-surface {
    perspective: var(--book-closed-perspective);
    transform-style: preserve-3d;
    pointer-events: none; /* Let ribbon clicks pass through (matches Book state fix) */
}

.book-desk-scene--play[data-play-mode="open"] .book-frame {
    animation: frameOpenSettle calc(var(--timeline-play-duration, 1.4s) * 0.5)
                   linear
                   calc(var(--timeline-play-duration, 1.4s) * 0.35) both,
               timelineFrameWidthDefault calc(var(--timeline-play-duration, 1.4s) * 0.35)
                   linear
                   calc(var(--timeline-play-duration, 1.4s) * 0.1) both !important;
    animation-play-state: running !important;
    transition: none !important;
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
}

/* Depth container: start Z-push at 20% of total so it synchronizes with the cover's
   Z-transition at perpendicular crossing (22-24% of total). The cover's translateZ
   drops from page-depth+2 to 0 at perpendicular — the container must push backward
   at the same time so the spine's Z-compensation stays aligned with the cover hinge.
   Previous 40% delay left a 16% gap (0.54s) where the cover was at Z=0 but the spine
   was still at Z=page-depth+2, creating a visible ~50px hinge disconnection. */
.book-desk-scene--play[data-play-mode="open"] .book-3d-depth {
    animation: timelineDepthFade calc(var(--timeline-play-duration, 3.4s) * 0.20)
               ease calc(var(--timeline-play-duration, 3.4s) * 0.20) both !important;
    animation-play-state: running !important;
    transition: none !important;
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: none;
    transform-style: preserve-3d;
}

/* Depth children geometry — delay = 35% of total so depth elements don't begin
   transitioning until the cover swing is ~94% complete (~-175deg). Combined with
   the 30% keyframe hold inside the 40% duration, visible movement doesn't start
   until ~47% of total — after the cover has essentially finished swinging.
   Previous 10% delay caused pages/spine to shift while the cover was at only -85deg. */
.book-desk-scene--play[data-play-mode="open"] .book-3d-page {
    animation: timelineDepthPage calc(var(--timeline-play-duration, 1.4s) * 0.4)
               linear
               calc(var(--timeline-play-duration, 1.4s) * 0.35) both !important;
    animation-play-state: running !important;
    transition: none !important;
    opacity: 1 !important;
    visibility: visible !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-3d-spine {
    animation: timelineDepthSpine calc(var(--timeline-play-duration, 1.4s) * 0.4)
               linear
               calc(var(--timeline-play-duration, 1.4s) * 0.35) both !important;
    animation-play-state: running !important;
    transition: none !important;
    /* opacity controlled by keyframe (stays 1 until 85%, fades to 0 by 100%) */
    visibility: visible !important;
    --book-spine-curvature: var(--book-open-spine-curvature, 130deg);
    /* Static Z-compensation: full push counteract. Any mismatch during the
       first ~10% (before container push starts) is hidden by the cover. */
    transform:
        translateZ(calc(var(--book-open-page-depth) + 2px))
        translateX(var(--book-spine-extra-tx, 0px))
        translateY(var(--book-spine-extra-ty, 0px))
        translateZ(var(--book-spine-extra-tz, 0px))
        rotateX(var(--book-spine-extra-rx, 0deg))
        rotateY(calc(90deg + var(--book-spine-extra-ry, 0deg)))
        rotateZ(var(--book-spine-extra-rz, 0deg));
    --book-spine-curvature: var(--book-open-spine-curvature, 130deg);
}
/* Back cover container: stays visible with preserve-3d for edge children.
   The face child handles the depth fade animation independently. */
.book-desk-scene--play[data-play-mode="open"] .book-3d-back-cover {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transition: none !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-3d-edge-right {
    animation: timelineDepthEdgeRight calc(var(--timeline-play-duration, 1.4s) * 0.4)
               linear
               calc(var(--timeline-play-duration, 1.4s) * 0.35) both !important;
    animation-play-state: running !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-3d-edge-top {
    animation: timelineDepthEdgeTop calc(var(--timeline-play-duration, 1.4s) * 0.4)
               linear
               calc(var(--timeline-play-duration, 1.4s) * 0.35) both !important;
    animation-play-state: running !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-3d-edge-bottom {
    animation: timelineDepthEdgeBottom calc(var(--timeline-play-duration, 1.4s) * 0.4)
               linear
               calc(var(--timeline-play-duration, 1.4s) * 0.35) both !important;
    animation-play-state: running !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
    transform-style: preserve-3d;
}

.book-desk-scene--play[data-play-mode="open"] .book-frame-right {
    width: 100%;
    flex: 1;
    transform-style: preserve-3d;
    overflow: visible;
    z-index: auto; /* Reset desktop z-index:2 — same fix as scrub mode */
}

.book-desk-scene--play[data-play-mode="open"] .book-viewport,
.book-desk-scene--play[data-play-mode="open"] .book-page-stack {
    transform-style: preserve-3d;
}

/* ── DOF overlay — fades in from 35-70% so background blurs as book opens ── */
@keyframes dofRevealOpen {
    0%, 35% { opacity: 0; }
    70%, 100% { opacity: 1; }
}

.book-desk-scene--play[data-play-mode="open"] .book-desk-effects::after {
    animation: dofRevealOpen var(--timeline-play-duration, 3.4s) ease-out forwards !important;
    animation-play-state: running !important;
}

/* ── Viewport cream reveal — delayed to 40% so the cream background (at Z=0) doesn't
   Z-fight with the rotating cover during the first half of the swing. Previous 22%
   delay placed the cream surface at the exact angle where cover center-Z ≈ 0,
   causing rapid compositor flicker in the preserve-3d context. ── */
.book-desk-scene--play[data-play-mode="open"] .book-viewport {
    perspective: none;
    background: transparent;
    animation: viewportCreamReveal calc(var(--timeline-play-duration, 3.4s) * 0.16)
               linear calc(var(--timeline-play-duration, 3.4s) * 0.40) both !important;
    animation-play-state: running !important;
}

/* ── Gutter shadow reveal — delayed to 42% (2% after cream) to avoid Z-fighting ── */
.book-desk-scene--play[data-play-mode="open"] .book-viewport::before {
    content: '';
    animation: gutterShadowReveal calc(var(--timeline-play-duration, 3.4s) * 0.16)
               linear calc(var(--timeline-play-duration, 3.4s) * 0.42) both !important;
    animation-play-state: running !important;
}

/* Nav and ribbons visible from start — attached to pages, occluded by same
   cover/flyleaf z-index stacking. No fade-in animation needed. */
.book-desk-scene--play[data-play-mode="open"] .book-nav,
.book-desk-scene--play[data-play-mode="open"] .book-nav-ribbons {
    opacity: 1 !important;
    pointer-events: auto !important;
}
/* Page-attached elements visible from start — matches scrub mode.
   No longer needs pageRevealOpen override since parent .book-page
   is now visibility:visible (cover/flyleaf z-index occludes these). */
.book-desk-scene--play[data-play-mode="open"] .book-page-sticky-notes,
.book-desk-scene--play[data-play-mode="open"] .book-page-ribbon-container,
.book-desk-scene--play[data-play-mode="open"] .book-page-ribbon-container-back {
    opacity: 1 !important;
    visibility: visible !important;
}

/* ── Spine glow — scrub track: glow 0-50% (duration 50%) ──
   Spine body stays hidden (cover obscures it), but the glow pseudo-element
   is visible and animates the warm light that leaks from the spine gap. */
.book-desk-scene--play[data-play-mode="open"] .book-spine-3d {
    opacity: 0 !important;
    visibility: hidden !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-spine-3d-glow {
    opacity: 1 !important;
    visibility: visible !important;
    animation: coverOpenSpineGlow calc(var(--timeline-play-duration, 3.4s) * 0.50)
               linear both !important;
    animation-play-state: running !important;
}

/* ── Frame ambient glow — scrub track: ambient 0-63% (duration 63%) ──
   ::before pseudo-element provides the warm radial glow around the frame. */
.book-desk-scene--play[data-play-mode="open"] .book-frame::before {
    content: '';
    display: block;
    position: absolute;
    inset: -40px;
    z-index: -1;
    pointer-events: none;
    animation: bookOpenAmbient calc(var(--timeline-play-duration, 3.4s) * 0.63)
               linear both !important;
    animation-play-state: running !important;
}

/* ── Frame ground shadow → breathe settle — scrub track: breathe 87-100% (duration 13%) ──
   ::after is repurposed from ground shadow (hidden during open) to breathe settle.
   Starts hidden (opacity 0), the bookSettleBreathe animation handles reveal at 87%. */
.book-desk-scene--play[data-play-mode="open"] .book-frame::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    z-index: -1;
    opacity: 0;
    visibility: visible;
    animation: bookSettleBreathe calc(var(--timeline-play-duration, 3.4s) * 0.13)
               linear calc(var(--timeline-play-duration, 3.4s) * 0.87) both !important;
    animation-play-state: running !important;
}
/* Hardcover removed — 3D back cover provides the leather border throughout.
   Frame-level .book-3d-back-edge strips provide perpendicular borders. */
.book-desk-scene--play[data-play-mode="open"] .book-3d-back-edge {
    opacity: 1 !important;
    visibility: visible !important;
}

/* Back cover edges stay visible (at .book-frame level, don't slide) */
.book-desk-scene--play[data-play-mode="open"] .book-3d-back-edge {
    opacity: 1 !important;
    visibility: visible !important;
}

/* Back cover face stays visible — Z transitions via timelineDepthBackCover. */
.book-desk-scene--play[data-play-mode="open"] .book-3d-back-cover-face {
    animation: timelineDepthBackCover calc(var(--timeline-play-duration, 1.4s) * 0.4)
               linear
               calc(var(--timeline-play-duration, 1.4s) * 0.35) both !important;
    animation-play-state: running !important;
    transition: none !important;
}

/* ── Particle burst (motes) — scrub track: motes 9-59% (duration 50%) ──
   16 motes with unique trajectories via --mote-dx/--mote-dy per element. */
.book-desk-scene--play[data-play-mode="open"] .book-open-mote {
    animation: openBurstMote calc(var(--timeline-play-duration, 3.4s) * 0.50)
               linear calc(var(--timeline-play-duration, 3.4s) * 0.09) both !important;
    animation-play-state: running !important;
}

/* Pages visible from start — occluded by cover (z:100) and flyleaves (z:90-66)
   in preserve-3d. Matches scrub-mode .book-page rule in the open section. Previous pageRevealOpen
   delayed reveal caused !important cascade conflicts and visibility:hidden
   propagation that hid page children (ribbons, sticky notes, content). */
.book-desk-scene--play[data-play-mode="open"] .book-page {
    opacity: 1 !important;
    visibility: visible !important;
    z-index: 50 !important;
    pointer-events: none;
}

/* Welcome content is now static (opacity: 1 by default) — no play-mode override needed */

/* Theme ribbon toggle — visible from start, part of the book's physical structure */
.book-desk-scene--play[data-play-mode="open"] .theme-ribbon {
    opacity: 1 !important;
    visibility: visible !important;
}

/* Flyleaf cascade — staggered to match scrub tracks (fly1: 26-53%, fly2: 34-60%, fly3: 41-68%, fly4: 49-75%) */
.book-desk-scene--play[data-play-mode="open"] .book-flyleaf {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    animation: flyleafTurnOpen calc(var(--timeline-play-duration, 3.4s) * 0.27)
               linear both !important;
    animation-play-state: running !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-flyleaf--1 {
    animation-delay: calc(var(--timeline-play-duration, 3.4s) * 0.26) !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-flyleaf--2 {
    animation-delay: calc(var(--timeline-play-duration, 3.4s) * 0.34) !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-flyleaf--3 {
    animation-delay: calc(var(--timeline-play-duration, 3.4s) * 0.41) !important;
}
.book-desk-scene--play[data-play-mode="open"] .book-flyleaf--4 {
    animation-delay: calc(var(--timeline-play-duration, 3.4s) * 0.49) !important;
}

/* Left-stack animated reveal — same timing as depth children */
.book-desk-scene--play[data-play-mode="open"] .book-3d-left-stack {
    animation: timelineLeftStackReveal calc(var(--timeline-play-duration, 1.4s) * 0.4)
               linear
               calc(var(--timeline-play-duration, 1.4s) * 0.1) both !important;
    animation-play-state: running !important;
    transition: none !important;
    pointer-events: none;
}

/* Ribbon fade — matches depth track timing */
.book-desk-scene--play[data-play-mode="open"] .book-3d-ribbons {
    animation: timelineRibbonFade calc(var(--timeline-play-duration, 3.4s) * 0.35)
               linear calc(var(--timeline-play-duration, 3.4s) * 0.24) both !important;
    animation-play-state: running !important;
    transition: none !important;
}

@media (min-width: 1200px) {
    .book-desk-scene--play[data-play-mode="open"] .book-frame {
        animation-name: frameOpenSettle, timelineFrameWidthDesktop !important;
        height: min(88vh, 800px);
    }

    /* Frame-right: animated width 100% → 50%, anchored right.
       Same stagger as frame width: starts at 10%, duration 35% of total.
       Completes before cover swing ends — hidden by 3D cover rotation. */
    .book-desk-scene--play[data-play-mode="open"] .book-frame-right {
        animation: timelineFrameRightWidth calc(var(--timeline-play-duration, 1.4s) * 0.35)
                       linear
                       calc(var(--timeline-play-duration, 1.4s) * 0.1) both !important;
        animation-play-state: running !important;
        flex: none !important;
        /* margin-left animated via timelineFrameRightWidth keyframe (0% → 50%) */
        transition: none !important;
    }

    /* Desktop back cover left constraint: tracks frame-right's margin-left exactly.
       Without this, back cover at left:0 extends into the left half as frame expands.
       Timing matches frame-right: delay 0.1, duration 0.35 of play duration. */
    .book-desk-scene--play[data-play-mode="open"] .book-3d-back-cover {
        animation: timelineDepthBackCoverOpen calc(var(--timeline-play-duration, 1.4s) * 0.35)
                       linear
                       calc(var(--timeline-play-duration, 1.4s) * 0.1) both !important;
        animation-play-state: running !important;
    }
}

/* ── Play: Close ── */
.book-desk-scene--play[data-play-mode="close"] .book-desk-surface {
    perspective: var(--book-closed-perspective);
    transform-style: preserve-3d;
}

/* Frame: flat → closed rotation (50-100% of total) + default width */
.book-desk-scene--play[data-play-mode="close"] .book-frame {
    animation: timelineFrameClose calc(var(--timeline-play-duration, 3s) * 0.5)
                   linear
                   calc(var(--timeline-play-duration, 3s) * 0.5) both,
               timelineFrameWidthDefault calc(var(--timeline-play-duration, 3s) * 0.4)
                   linear
                   calc(var(--timeline-play-duration, 3s) * 0.6) both !important;
    animation-play-state: running !important;
    transition: none !important;
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
}

.book-desk-scene--play[data-play-mode="close"] .book-cover-page {
    animation: coverPageClose calc(var(--timeline-play-duration, 3s) * 0.6)
               linear
               calc(var(--timeline-play-duration, 3s) * 0.4) both !important;
    animation-play-state: running !important;
    transition: none !important;
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    top: calc(-1 * var(--book-cover-overhang-top));
    left: 0;
    right: calc(-1 * var(--book-cover-overhang-right));
    bottom: calc(-1 * var(--book-cover-overhang-bottom));
}

/* Flyleaf reverse cascade — staggered to match close scrub tracks (fly4: 0-30%, fly3: 7-37%, fly2: 14-44%, fly1: 20-50%) */
.book-desk-scene--play[data-play-mode="close"] .book-flyleaf {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    transform: translateZ(var(--flyleaf-z-open, 2px)) rotateY(-180deg);
    animation: flyleafTurnClose calc(var(--timeline-play-duration, 3s) * 0.30)
               linear both !important;
    animation-play-state: running !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-flyleaf--4 {
    animation-delay: 0s !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-flyleaf--3 {
    animation-delay: calc(var(--timeline-play-duration, 3s) * 0.07) !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-flyleaf--2 {
    animation-delay: calc(var(--timeline-play-duration, 3s) * 0.14) !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-flyleaf--1 {
    animation-delay: calc(var(--timeline-play-duration, 3s) * 0.20) !important;
}

/* Depth container: animated Z-push removal synchronized with children (50-100% of total).
   Without this, desk-scene.css's .book-desk-scene--closing .book-3d-depth transition
   bleeds through (not gated by :not(.book-desk-scene--play)), starting at 0.8s while
   children's play-mode keyframes don't start until 50% (1.5s). The 0.7s desync causes
   spine/edges to protrude forward, visibly separating from the cover. */
.book-desk-scene--play[data-play-mode="close"] .book-3d-depth {
    animation: timelineDepthFadeClose calc(var(--timeline-play-duration, 3s) * 0.5)
               linear
               calc(var(--timeline-play-duration, 3s) * 0.5) both !important;
    animation-play-state: running !important;
    transition: none !important;
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: none;
    transform-style: preserve-3d;
}

/* Depth children: open → closed geometry (50-100% of total).
   Same visibility enforcement as open mode. */
.book-desk-scene--play[data-play-mode="close"] .book-3d-page {
    animation: timelineDepthPageClose calc(var(--timeline-play-duration, 3s) * 0.5)
               linear
               calc(var(--timeline-play-duration, 3s) * 0.5) both !important;
    animation-play-state: running !important;
    transition: none !important;
    opacity: 1 !important;
    visibility: visible !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-3d-spine {
    animation: timelineDepthSpineClose calc(var(--timeline-play-duration, 3s) * 0.5)
               linear
               calc(var(--timeline-play-duration, 3s) * 0.5) both !important;
    animation-play-state: running !important;
    transition: none !important;
    --book-spine-curvature: var(--book-open-spine-curvature, 130deg);
    /* transform now controlled by keyframe — Z-compensation decreases from
       pd+2 to 0 by 70%, matching the depth container's Z-push removal.
       No static transform override needed (old static Z-comp caused the
       spine to protrude forward as the container Z approached 0). */
    visibility: visible !important;
}
/* Prevent desk-scene.css spine-strip transition (1.4s + 0.8s delay) from
   interfering with spine geometry during play mode close animation. */
.book-desk-scene--play[data-play-mode="close"] .book-3d-spine-strip {
    transition: none !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-3d-back-cover {
    animation: timelineDepthBackCoverClose calc(var(--timeline-play-duration, 3s) * 0.5)
               linear
               calc(var(--timeline-play-duration, 3s) * 0.5) both !important;
    animation-play-state: running !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-3d-edge-right {
    animation: timelineDepthEdgeRightClose calc(var(--timeline-play-duration, 3s) * 0.5)
               linear
               calc(var(--timeline-play-duration, 3s) * 0.5) both !important;
    animation-play-state: running !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-3d-edge-top {
    animation: timelineDepthEdgeTopClose calc(var(--timeline-play-duration, 3s) * 0.5)
               linear
               calc(var(--timeline-play-duration, 3s) * 0.5) both !important;
    animation-play-state: running !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-3d-edge-bottom {
    animation: timelineDepthEdgeBottomClose calc(var(--timeline-play-duration, 3s) * 0.5)
               linear
               calc(var(--timeline-play-duration, 3s) * 0.5) both !important;
    animation-play-state: running !important;
    transition: none !important;
    /* opacity controlled by keyframe */
    visibility: visible !important;
}

/* Left-stack: hide during close */
.book-desk-scene--play[data-play-mode="close"] .book-3d-left-stack {
    animation: timelineLeftStackHide calc(var(--timeline-play-duration, 3s) * 0.5)
               linear
               calc(var(--timeline-play-duration, 3s) * 0.5) both !important;
    animation-play-state: running !important;
    transition: none !important;
    pointer-events: none;
}

/* Ribbon reveal — becomes visible as book closes */
.book-desk-scene--play[data-play-mode="close"] .book-3d-ribbons {
    animation: timelineRibbonReveal calc(var(--timeline-play-duration, 3s) * 0.36)
               linear calc(var(--timeline-play-duration, 3s) * 0.40) both !important;
    animation-play-state: running !important;
    transition: none !important;
}

/* Flat 2D spine stays hidden during close — the 3D spine handles rendering */
.book-desk-scene--play[data-play-mode="close"] .book-spine-3d {
    opacity: 0 !important;
    visibility: hidden !important;
}
.book-desk-scene--play[data-play-mode="close"] .book-frame::after {
    opacity: 1;
    visibility: visible;
}
/* Hardcover removed — 3D back cover handles close animation border. */

/* ── DOF overlay — fades out 0-40% as book closes ── */
@keyframes dofRevealClose {
    0% { opacity: 1; }
    40%, 100% { opacity: 0; }
}

.book-desk-scene--play[data-play-mode="close"] .book-desk-effects::after {
    animation: dofRevealClose var(--timeline-play-duration, 3s) ease-in forwards !important;
    animation-play-state: running !important;
}

/* Pages hidden during close play */
.book-desk-scene--play[data-play-mode="close"] .book-page {
    opacity: 0 !important;
    visibility: hidden !important;
}

/* Desktop: actual frame width contraction */
@media (min-width: 1200px) {
    .book-desk-scene--play[data-play-mode="close"] .book-frame {
        animation-name: timelineFrameClose, timelineFrameWidthClose !important;
        height: min(88vh, 800px);
    }

    .book-desk-scene--play[data-play-mode="close"] .book-frame-right {
        animation: timelineFrameRightClose calc(var(--timeline-play-duration, 3s) * 0.4)
                       linear
                       calc(var(--timeline-play-duration, 3s) * 0.6) both !important;
        animation-play-state: running !important;
        flex: none !important;
        transition: none !important;
        z-index: auto; /* Reset desktop z-index:2 — same fix as scrub mode */
    }

    /* Desktop spine close: override keyframe to include `right` animation.
       Spine uses right:100% in base CSS (left edge of depth container). On desktop,
       the depth container fills the full frame but visible area is only the right half.
       right must animate from 50% (open center) to 100% (closed left edge), synced
       with frame-right expansion at kf 20-100%. 70% step: 81.25% = linear at 62.5%. */
    .book-desk-scene--play[data-play-mode="close"] .book-3d-spine {
        animation-name: timelineDepthSpineCloseDesktop !important;
    }

    /* Desktop close back cover: use play-mode keyframe with 20% left-hold.
       Scrub mode retains the original timelineDepthBackCoverClose keyframe
       (both tracks advance together, so no hold needed). */
    .book-desk-scene--play[data-play-mode="close"] .book-3d-back-cover {
        animation-name: timelineDepthBackCoverCloseDesktopPlay !important;
    }

    /* Desktop back-edge head/tail close: animate left from 50% to board-thickness.
       Same 20-100% keyframe sync as back cover and spine. These are frame-level
       elements that span the full frame in open state but should only span the
       visible right half. */
    .book-desk-scene--play[data-play-mode="close"] .book-3d-back-edge--head,
    .book-desk-scene--play[data-play-mode="close"] .book-3d-back-edge--tail {
        animation: timelineBackEdgeLeftClose calc(var(--timeline-play-duration, 3s) * 0.5)
                       linear
                       calc(var(--timeline-play-duration, 3s) * 0.5) both !important;
        animation-play-state: running !important;
        transition: none !important;
    }

    /* Desktop close: hide edge-top, edge-bottom, and left-stack to prevent 3D
       compositing ghosting. These are positioned for the full-width frame but the
       visible area is only the right half. Edges are at opacity:0 until 94% of their
       keyframe (barely one frame before state changes to Cover), and left-stack fades
       at wrong position. visibility:hidden removes them from the preserve-3d composite
       entirely. After play mode drops, Cover state CSS shows them at closed positions. */
    .book-desk-scene--play[data-play-mode="close"] .book-3d-edge-top,
    .book-desk-scene--play[data-play-mode="close"] .book-3d-edge-bottom,
    .book-desk-scene--play[data-play-mode="close"] .book-3d-left-stack {
        opacity: 0 !important;
        visibility: hidden !important;
    }
}

/* ── Play: Pickup ── */
.book-desk-scene--play[data-play-mode="pickup"] .book-desk-surface {
    perspective: var(--book-table-perspective);
    transform-style: preserve-3d;
}

.book-desk-scene--play[data-play-mode="pickup"] .book-frame {
    animation: timelinePickupFrame var(--timeline-play-duration, 1.2s) linear both !important;
    animation-play-state: running !important;
    transition: none !important;
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
}

.book-desk-scene--play[data-play-mode="pickup"] .book-3d-depth {
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: none;
    transform-style: preserve-3d;
}

/* ── Play: Set Down ── */
.book-desk-scene--play[data-play-mode="setdown"] .book-desk-surface {
    perspective: var(--book-closed-perspective);
    transform-style: preserve-3d;
}

.book-desk-scene--play[data-play-mode="setdown"] .book-frame {
    animation: timelineSetdownFrame var(--timeline-play-duration, 1s) linear both !important;
    animation-play-state: running !important;
    transition: none !important;
    transform-style: preserve-3d;
    overflow: visible;
    isolation: auto;
}

.book-desk-scene--play[data-play-mode="setdown"] .book-3d-depth {
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: none;
    transform-style: preserve-3d;
}

/* ── Play: Flyleaf ── */
.book-desk-scene--play[data-play-mode="flyleaf"] .book-desk-surface {
    perspective: var(--book-closed-perspective);
    transform-style: preserve-3d;
}

.book-desk-scene--play[data-play-mode="flyleaf"] .book-flyleaf {
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
    animation: flyleafTurnOpen calc(var(--timeline-play-duration, 2.1s) * 0.43) linear both !important;
    animation-play-state: running !important;
}

.book-desk-scene--play[data-play-mode="flyleaf"] .book-cover-page {
    transform: translateZ(0px) rotateY(-180deg) !important;
    opacity: 1 !important;
    visibility: visible !important;
    transform-style: preserve-3d;
}

/* ═══════════════════════════════════════════════════════════════
   REDUCED MOTION
   ═══════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
    .book-desk-scene--scrub .book-cover-page,
    .book-desk-scene--scrub .book-frame,
    .book-desk-scene--scrub .book-frame-right,
    .book-desk-scene--scrub .book-3d-depth,
    .book-desk-scene--scrub .book-3d-depth > *,
    .book-desk-scene--scrub .book-flyleaf,
    .book-desk-scene--play .book-cover-page,
    .book-desk-scene--play .book-frame,
    .book-desk-scene--play .book-frame-right,
    .book-desk-scene--play .book-3d-depth,
    .book-desk-scene--play .book-3d-depth > *,
    .book-desk-scene--play .book-flyleaf {
        animation-duration: 0.01ms !important;
    }
}
