/* ═══════════════════════════════════════════
   gift-of-flowers — the shell (build contract §7.2; screens.md, cross-beat contract)

   The stage, its three set-piece canvases, the world that holds the shared
   ground and every beat's <section>, the mute control, and the few rules the
   film spine (js/app.js) relies on. Beats style only their own prefixed
   classes; .screen, the stage, the canvases and the ground belong here.

   Stacking inside #stage (all position: absolute over one box):
     0  #fx-back        the ambient bokeh canvas (film.ambient; never turned on here)
     1  #world          the shared ground (#gof-ground), then every beat, in order
     2  #fx-light       (no ReveelHero shot plays in this film)
     4  #fx-burst
     8  #stage-mute

   Inside #world, back to front: #gof-ground (the wash with its mottling
   painted in, three planes of bubbles), then the sections, which are
   transparent except the cream paper the-gift and the-bloom paint for
   themselves.

   A light ground (concept.md, cream-blush): bubbles are DARKER than the wash,
   capped at --bubble-opacity-max (0.5), and sit under every text layer. Soft
   focus on the near plane is a wider radial feather, never filter: blur.
   ═══════════════════════════════════════════ */

html,
body {
    height: 100%;
    margin: 0;
}

body {
    background: var(--bg-elevated);
    color: var(--text-primary);
    font-family: var(--font-body);
    overflow: hidden;
    overscroll-behavior: none;
    -webkit-tap-highlight-color: transparent;
    -webkit-font-smoothing: antialiased;
}

button {
    font: inherit;
}

/* ───── The stage: ReveelStage's camera box, one phone screen tall ─────
   `overflow: clip`, not hidden: a hidden box is still a scroll container,
   and a scrollIntoView could scroll the whole film. */
#stage {
    width: 100%;
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
    overflow: clip;
    background-color: var(--bg-elevated);
    -webkit-user-select: none;
    user-select: none;
}

/* ───── The set-piece canvases: inert, covering the stage ───── */
.fx {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}
.fx-back { z-index: 0; }
.fx-light { z-index: 2; }
.fx-burst { z-index: 4; }

/* ───── The world ─────
   The camera is static for the whole film (shots.md: "ReveelStage.camera is
   not used"), and nothing in the world sits at a depth. So the world is one
   flat plane, and its transform is stage-25d's camera pose kept in 2D
   (x and y still follow film.camera; z and rotY are dropped): a static 2D
   transform is not a compositing reason, where the shared translate3d made
   the whole world a full-screen layer (11 MB at DPR 3) for the entire film. */
.reveel-stage-world {
    z-index: 1;
    transform-style: flat;
    transform: translate(var(--cam-x, 0px), var(--cam-y, 0px));
}

/* ═══════════ The shared ground (bridge-owned) ═══════════
   Memory, not just frames: on an iPhone the WebContent process is killed
   when composited backing stores run past its budget, so nothing in the
   ground is a full-screen layer. The wash and its mottling are one painted
   background (no layer of their own); each bubble is its own tiny layer
   (at most 26 px square, about 25 KB at DPR 3), alive only while the ground
   is bubbling. */
.gof-ground {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    contain: strict;
}

/* The blush wash and its white mottling, one static painted background,
   never animated. The three mottling blobs (concept.md: white radials at
   96vmax, 0.62 at the core) sit where they always stood; their 60-90 s
   drift (6-9 vw, barely perceptible) is dropped so they cost no layer. The
   wash's deepest stop is --bg-elevated; the mottling only lightens it. */
.gof-wash {
    position: absolute;
    inset: 0;
    background:
        radial-gradient(circle calc(var(--wash-mottle-size) / 2) at 15% 9%, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0)),
        radial-gradient(circle calc(var(--wash-mottle-size) / 2) at 88% 53%, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0)),
        radial-gradient(circle calc(var(--wash-mottle-size) / 2) at 24% 97%, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0)),
        var(--wash-ground);
}

/* ───── The bubbles ─────
   .gof-bubbles and the three plane containers are plain boxes: no opacity,
   no transform, no animation, so they never become layers (or render
   surfaces) of their own. the-letter slides .gof-bubbles with a temporary
   additive `translate` Web Animation on its exit (one layer for 1.4 s).

   Each bubble climbs from just below the bottom edge to just above the top
   (`transform`, linear: off screen at both ends, so the loop is seamless)
   at its plane's speed, and sways ±1.5 vw (`translate`, breath, alternate),
   both on the compositor and both on the bubble itself, so it is one small
   layer. Its negative delay (--gof-d, inline) puts it mid-climb at the first
   frame, so the field is full the moment it appears. Opacity is per bubble,
   the plane's: far 0.30, mid 0.40, near the 0.5 ceiling.

   Until #gof-ground.is-bubbling the bubbles are transparent with no
   animation at all (no layers while the cream paper covers the ground);
   with it they fade up over 1.2 s on `silk` and start climbing. */
.gof-bubbles,
.gof-bubbles-far,
.gof-bubbles-mid,
.gof-bubbles-near {
    position: absolute;
    inset: 0;
}
.gof-bubbles-far {
    --gof-o: 0.3;
    --gof-rise: 46s;              /* a far bubble crosses the screen in 46 s */
    --gof-sway: 13s;
}
.gof-bubbles-mid {
    --gof-o: 0.4;
    --gof-rise: 34s;
    --gof-sway: 10s;
}
.gof-bubbles-near {
    --gof-o: var(--bubble-opacity-max);
    --gof-rise: 26s;
    --gof-sway: 8s;
}

.gof-bubble {
    position: absolute;
    top: 100%;                    /* its climb starts just below the bottom edge */
    width: var(--gof-b);
    height: var(--gof-b);
    margin-left: calc(var(--gof-b) * -0.5);
    border-radius: 50%;
    background: var(--bubble);
    opacity: 0;
    transition: opacity 1.2s var(--ease-silk);
}
#gof-ground.is-bubbling .gof-bubble {
    opacity: min(var(--gof-o), var(--bubble-opacity-max));
    animation:
        gof-bubble-rise var(--gof-rise) linear var(--gof-d) infinite both,
        gof-bubble-sway var(--gof-sway) var(--ease-breath) var(--gof-sd) infinite alternate both;
}

/* 100vh (the large viewport on iOS) plus the largest bubble: the climb
   always ends above the top edge, whatever the toolbar is doing. */
@keyframes gof-bubble-rise {
    from { transform: translate3d(0, 0, 0); }
    to { transform: translate3d(0, calc(-100vh - 32px), 0); }
}
@keyframes gof-bubble-sway {
    from { translate: -1.5vw 0; }
    to { translate: 1.5vw 0; }
}

/* Near: soft focus by a wider radial feather (never filter: blur). The
   darkest stop is --bubble-lo (#eba3b5), feathered to nothing at the rim. */
.gof-bubbles-near .gof-bubble {
    background: radial-gradient(circle at 40% 36%,
        var(--bubble-hi) 0%,
        var(--bubble-mid) 34%,
        var(--bubble-lo) 56%,
        rgba(235, 163, 181, 0.45) 68%,
        rgba(235, 163, 181, 0) 76%);
}

/* ───── Beats ─────
   Every beat is a full-bleed layer of the world, the stage's size. Only the
   director changes which one is active: a hard cut on the beat boundary
   (each beat's exit composes the frame for the next and its entrance cue
   reveals it), so there is no fade and no entrance stagger. */
.screen {
    position: absolute;
    inset: 0;
    display: none;
    opacity: 0;
}
.screen.active {
    display: flex;
    opacity: 1;
}

/* Entry states are applied instantly (the spine's prepare on goto), never
   animated in. */
.instant,
.instant *,
.instant *::before,
.instant *::after {
    transition: none !important;
}

/* ───── Mute ───── */
.stage-mute {
    position: absolute;
    z-index: 8;
    top: calc(14px + env(safe-area-inset-top, 0px));
    right: 14px;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    min-height: 44px;
    padding: 0.5rem 0.85rem;
    font: 500 var(--type-hint)/1 var(--font-body);
    letter-spacing: 0.14em;
    text-transform: lowercase;
    color: var(--wine);
    background: var(--glass);
    border: 1px solid var(--glass-border);
    border-radius: 999px;
    cursor: pointer;
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
}
.stage-mute-icon {
    width: 15px;
    height: 15px;
}
.stage-mute .stage-mute-x { display: none; }
.stage-mute[aria-pressed="true"] .stage-mute-x { display: inline; }
.stage-mute[aria-pressed="true"] .stage-mute-waves { display: none; }
.stage-mute[aria-pressed="true"] {
    color: var(--text-primary);
    border-color: var(--glass-border-hover);
}
.stage-mute:focus-visible {
    outline: 2px solid var(--wine);
    outline-offset: 3px;
}

/* ───── Reduced motion: the film still plays in full, calmly ─────
   The bubbles hold still at 0.4 opacity, each resting where the moving
   field would show it at its first frame (--gof-y, inline). They still
   fade up with the ground; nothing climbs or sways, so no layers. */
@media (prefers-reduced-motion: reduce) {
    .gof-bubble {
        top: var(--gof-y);
        margin-top: calc(var(--gof-b) * -0.5);
    }
    #gof-ground.is-bubbling .gof-bubble {
        opacity: 0.4;
        animation: none;
    }
}
