/* ==========================================================================
   Digital3D Hero Cinema — post-processing stack for the 10 hero scenes
   8 full-box layers stacked above .hero-anim (z-index 20 → 27)
   Budget : 12 KB CSS, 0 JS, and AT MOST ONE NETWORK image for the whole page —
   a page shows ONE scene, and each of the 10 presets names its own plate
   (--grade-plate-src, 20-60 KB), so a visit fetches exactly one of them.
   The fail2ban jail counts the VOLUME of static requests, not their status
   codes: never give a layer a second raster. .grade-grain therefore carries its
   texture as an inline data-URI (a few hundred bytes, 0 requests); any further
   texture must follow that route, not a new file under wwwroot.
   --------------------------------------------------------------------------
   NEUTRAL BY CONSTRUCTION — every layer takes its opacity from a --grade-*
   intensity whose fallback is 0, and each .hero-scene-N preset declares the
   whole set at 0. At opacity 0 a layer composites to the backdrop untouched,
   blend modes included: until a preset raises an intensity the hero renders
   exactly as it did before this file existed.

   Performance rules — do not "optimise" them away: no `will-change` on the
   static layers (a promoted full-box layer costs 2.3 MB, 9.2 MB at DPR 2), no
   `backdrop-filter` (it snapshots and blurs 1920×300 every frame), the only
   blur sits on the STATIC .grade-bloom so it rasterises once, and the only two
   animated layers — .grade-rays and .grade-grain — take their animation-name
   from --grade-rays-anim / --grade-grain-anim (default `none`), so nothing ticks
   while a scene leaves them off. Both animate transform, and gradeRaysDrift also
   animates opacity — both are compositor properties: never a repaint, and never
   an animated filter (which is the rule that matters, see T7).

   TRAP — url() inside a custom property is resolved against the DOCUMENT, not
   against this stylesheet. --grade-plate-src is therefore root-absolute:
   url("/Images/hero/ocean-plate.webp"), never a relative "../Images/…".
   ========================================================================== */

/* Shared frame. The layers are siblings of .hero-anim, never its children:
   hero-animations.css halves the particles with :nth-child(2n), counted among
   all siblings — nesting here would flip the parity. */
.hero-stage > [class^="grade-"] {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

/* 20. HAZE — aerial perspective: fogging the band around the horizon line is
   what pushes the far planes back. Static. */
.hero-stage > .grade-haze {
    z-index: 20;
    /* BASELINE — free of color-mix(), so it computes everywhere. The softer
       variant is applied by the @supports block below, NOT by a second
       declaration here: a value containing var() always PARSES, so it would win
       the cascade and then fail at computed-value time, dropping `background` to
       its initial `none` — the haze would vanish instead of falling back. */
    background: linear-gradient(to bottom, transparent 0%,
        var(--grade-haze-color, #b9c6d6) calc(var(--hero-horizon, 66%) - 10%),
        var(--grade-haze-color, #b9c6d6) var(--hero-horizon, 66%), transparent 100%);
    opacity: var(--grade-haze, 0);
}

/* ENHANCEMENT — the same band with its two ends thinned out, which is what makes
   the haze read as aerial perspective rather than a grey bar. Gated on real
   support; without it the baseline above stands. */
@supports (color: color-mix(in srgb, red, blue)) {
    .hero-stage > .grade-haze {
        background: linear-gradient(to bottom, transparent 0%,
            color-mix(in srgb, var(--grade-haze-color, #b9c6d6) 55%, transparent) calc(var(--hero-horizon, 66%) - 10%),
            var(--grade-haze-color, #b9c6d6) var(--hero-horizon, 66%),
            color-mix(in srgb, var(--grade-haze-color, #b9c6d6) 20%, transparent) 100%);
    }
}

/* 21. PLATE — photographic texture (grain / dust / halation). One raster plate
   per graded scene, named by --grade-plate-src: a scene that leaves the variable
   unset resolves to `none` and issues NO network request.
   --grade-plate-size defaults to `cover`, but a plate whose light is AIMED at a
   feature of the scene must be stretched, not cropped: `cover` on the 6.4:1 stage
   would keep only the middle 47 % of a 3:1 plate and throw its top away. Nothing
   in a pure grain/light texture has a shape to distort, so `100% 100%` is free. */
.hero-stage > .grade-plate {
    z-index: 21;
    background-image: var(--grade-plate-src, none);   /* root-absolute, see TRAP */
    background-size: var(--grade-plate-size, cover);
    background-repeat: no-repeat;
    mix-blend-mode: var(--grade-plate-blend, soft-light);
    opacity: var(--grade-plate, 0);
}

/* 22. BLOOM — halos hand-placed on the light sources of a scene. Static, so
   the blur is rasterised once. Blooms 2 and 3 default to `transparent`. */
.hero-stage > .grade-bloom {
    z-index: 22;
    background:
        radial-gradient(circle at var(--grade-bloom-x, 50%) var(--grade-bloom-y, 35%),
            var(--grade-bloom-color, transparent) 0%, transparent var(--grade-bloom-r, 18%)),
        radial-gradient(circle at var(--grade-bloom2-x, 20%) var(--grade-bloom2-y, 30%),
            var(--grade-bloom2-color, transparent) 0%, transparent var(--grade-bloom2-r, 12%)),
        radial-gradient(circle at var(--grade-bloom3-x, 80%) var(--grade-bloom3-y, 40%),
            var(--grade-bloom3-color, transparent) 0%, transparent var(--grade-bloom3-r, 12%));
    filter: blur(calc(var(--hero-h, 300px) * .06));
    mix-blend-mode: screen;
    opacity: var(--grade-bloom, 0);
}

/* 23. RAYS — volumetric light: five slits in a SINGLE layer (multi-background),
   faded out by a radial mask anchored on the light source. Only animated layer:
   ±0.8 % drift plus an opacity breath, both compositor-friendly. */
.hero-stage > .grade-rays {
    z-index: 23;
    background-image:
        linear-gradient(var(--grade-rays-angle, 100deg), transparent 12%, var(--grade-rays-color, #fff) 13%, transparent 15.5%),
        linear-gradient(var(--grade-rays-angle, 100deg), transparent 24%, var(--grade-rays-color, #fff) 25%, transparent 28%),
        linear-gradient(var(--grade-rays-angle, 100deg), transparent 39%, var(--grade-rays-color, #fff) 40%, transparent 41.5%),
        linear-gradient(var(--grade-rays-angle, 100deg), transparent 53%, var(--grade-rays-color, #fff) 54.5%, transparent 57%),
        linear-gradient(var(--grade-rays-angle, 100deg), transparent 70%, var(--grade-rays-color, #fff) 71%, transparent 74%);
    mask-image: radial-gradient(ellipse 65% 130% at var(--grade-rays-x, 50%) -15%, #000 0%, transparent 78%);
    mix-blend-mode: screen;
    opacity: var(--grade-rays, 0);
    /* `none` until a preset names the keyframes: nothing ticks while off. */
    animation: var(--grade-rays-anim, none) 17s ease-in-out infinite alternate;
}

@keyframes gradeRaysDrift {
    0%   { transform: translate3d(-0.8%, 0, 0); opacity: calc(var(--grade-rays, 0) * .78); }
    50%  { transform: translate3d(0, 0, 0);     opacity: var(--grade-rays, 0); }
    100% { transform: translate3d(0.8%, 0, 0);  opacity: calc(var(--grade-rays, 0) * .85); }
}

/* 24. TONE — the grade itself: cold shadows lifted toward a warm key. Static,
   and by far the biggest effect per byte of the whole stack. */
.hero-stage > .grade-tone {
    z-index: 24;
    background: linear-gradient(var(--grade-tone-angle, 155deg),
        var(--grade-shadow, #16213e) 0%, var(--grade-light, #ffd9a0) 100%);
    mix-blend-mode: var(--grade-tone-blend, soft-light);
    opacity: var(--grade-tone, 0);
}

/* 25. FRINGE — lens chromatic aberration, confined to the outer ~12 % where a
   real lens breaks down. Static, deliberately barely visible. */
.hero-stage > .grade-fringe {
    z-index: 25;
    background:
        radial-gradient(ellipse 12% 105% at 0% 50%, var(--grade-fringe-cool, #00d0ff) 0%, transparent 72%),
        radial-gradient(ellipse 12% 105% at 100% 50%, var(--grade-fringe-warm, #ff3b30) 0%, transparent 72%);
    mix-blend-mode: screen;
    opacity: var(--grade-fringe, 0);
}

/* 26. VIGNETTE — radii in %, on purpose: at a 6.4:1 aspect the horizontal one
   becomes huge in px, which is exactly what we want (centre and title safe
   area must not darken, only the far left/right edges do). */
.hero-stage > .grade-vignette {
    z-index: 26;
    background: radial-gradient(ellipse 75% 130% at 50% 45%, transparent 42%, #000 100%);
    opacity: var(--grade-vignette, 0);
}

/* 27. GRAIN — the film stock itself, hence LAST: haze/bloom/rays are the light,
   tone is the grade, fringe and vignette are the lens; grain is what the
   emulsion adds on top of all of them. It stays under .video-banner-overlay
   (z-index 1 in a stacking context ABOVE .hero-stage), so the title is clean.

   The one layer that must never be stretched. Grain is a HIGH-frequency signal:
   it only exists at native pixel scale, so it is tiled at a fixed px size and
   never sized in %. That is the whole difference with .grade-plate, whose light
   is low frequency and is meant to stretch onto the features of the scene — a
   1536×512 plate squeezed to 1920×300 (0.59 in Y) keeps its glow and loses its
   grain entirely, which is why the two live in separate layers.

   Zero network cost: the texture is a STATIC feTurbulence in a data-URI, so it
   never touches the fail2ban jail's request budget. Static is not a detail — a
   SMIL-animated feTurbulence is re-computed every frame over the full box, and
   the hero used to carry seven of them (a shared <defs> carrier, since removed
   as unreferenced); it is the one cost this stack must not reintroduce. This
   one rasterises once and is then served from the image cache like any bitmap.
   Inside the data-URI: color-interpolation-filters='sRGB' keeps the noise
   centred on 0.5 (the linearRGB default would land it near 0.74 and brighten
   the frame), filterUnits='userSpaceOnUse' pins the filter region to the tile so
   stitchTiles='stitch' actually stitches, the feColorMatrix desaturates and
   forces alpha to 1 (opaque mid-grey, not translucent black), and the
   feComponentTransfer expands contrast AROUND 0.5 (slope 1.9, intercept -0.45)
   so the layer stays neutral where the noise is neutral. The `#` of the internal
   filter reference MUST be %23 or the URI is cut at a fragment.

   `overlay`, not `screen`: grain has to modulate in BOTH directions. screen can
   only add light — right for .grade-plate, wrong here. */
.hero-stage > .grade-grain {
    z-index: 27;
    /* Overshoot the frame: the drift below shifts the layer by a few px and
       inset:0 would expose a bare edge. .video-banner clips the overshoot. */
    inset: -8px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='g' filterUnits='userSpaceOnUse' x='0' y='0' width='128' height='128' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.72' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='.33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='1.9' intercept='-.45'/%3E%3CfeFuncG type='linear' slope='1.9' intercept='-.45'/%3E%3CfeFuncB type='linear' slope='1.9' intercept='-.45'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23g)'/%3E%3C/svg%3E");
    background-size: var(--grade-grain-size, 128px) var(--grade-grain-size, 128px);
    background-repeat: repeat;
    mix-blend-mode: overlay;
    opacity: var(--grade-grain, 0);
    /* `none` until a preset names the keyframes: nothing ticks while off. */
    animation: var(--grade-grain-anim, none) 1s steps(1, end) infinite;
}

/* Moving grain is what separates "film" from "frozen noise". Nothing but a
   translate on an already-rasterised tile: compositor only, zero repaint, and
   no `will-change` (the animation itself promotes the layer for its duration). */
@keyframes gradeGrainShift {
    0%   { transform: translate3d(0, 0, 0); }
    20%  { transform: translate3d(-5px, 3px, 0); }
    40%  { transform: translate3d(4px, -4px, 0); }
    60%  { transform: translate3d(-3px, -2px, 0); }
    80%  { transform: translate3d(5px, 4px, 0); }
    100% { transform: translate3d(0, 0, 0); }
}

/* ==========================================================================
   PRESETS — one block per scene, holding the full variable set. Everything
   ships at intensity 0, so the colours are inert placeholders carrying the
   intent of the next step: grading a scene means raising its numbers HERE,
   never touching the engine above. The secondary bloom variables are left out
   of the presets: they default to `transparent` and paint nothing.
   ========================================================================== */

/* 1 — Aurora (graded)
   --hero-horizon is MEASURED, not chosen: the far crest line is y=224 of a
   `0 0 2400 300` viewBox stretched with preserveAspectRatio="none" over the full
   height, i.e. 224/300 = 74.7 % at every breakpoint. .grade-haze bands on it.
   Bloom and rays are anchored on the REAL light of the scene — the two curtain
   columns, whose mask holds them at ~13 % and ~87 % of the width — never on the
   centre, which the same mask keeps dim so the title reads. */
.hero-scene-1 {
    --hero-horizon: 74.7%;
    /* Kept low: the sky here is nearly black and the curtains are the only light.
       At Ocean's .34 the haze turned the whole lower sky into a flat grey fog and
       swallowed the far range — the same failure mode, one scene later. */
    --grade-haze: .15; --grade-haze-color: #2f5a78;
    --grade-plate: .17;
    --grade-plate-src: url("/Images/hero/aurora-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: screen;
    --grade-plate-size: 100% 100%;
    /* Three halos, all on real sources: the green column, the violet one, and the
       pool of aurora light on the snow under the green column. The curtains are
       already `screen` + blurred, so these are DELIBERATELY restrained — stacking
       a second glow on a glow is how a scene loses its blacks. */
    --grade-bloom: .38;
    --grade-bloom-x: 13%; --grade-bloom-y: 44%; --grade-bloom-r: 25%; --grade-bloom-color: #48ffc8;
    --grade-bloom2-x: 87%; --grade-bloom2-y: 42%; --grade-bloom2-r: 23%; --grade-bloom2-color: #b478ff;
    --grade-bloom3-x: 12%; --grade-bloom3-y: 84%; --grade-bloom3-r: 15%; --grade-bloom3-color: #3fd8b0;
    /* Nearly off: the front curtain already carries carved rays, and this layer's
       five slits would beat against them. It is here for the slow breath only. */
    --grade-rays: .05; --grade-rays-x: 13%; --grade-rays-color: #d6ffee; --grade-rays-anim: gradeRaysDrift;
    --grade-tone: .28; --grade-tone-blend: soft-light; --grade-shadow: #071535; --grade-light: #8ef0d4;
    --grade-fringe: .05; --grade-vignette: .40;
    /* Less than Ocean's .35: this frame has large bright areas in the wings, and
       grain reads twice as loud on light as it does on a black sky. */
    --grade-grain: .28; --grade-grain-anim: gradeGrainShift;
}

/* 2 — Sakura (graded: first of the two DAYLIGHT scenes)
   --hero-horizon is MEASURED: the far crest is the upper edge of .sm-far, a
   `0 0 2400 165` viewBox stretched with preserveAspectRatio="none" over a box
   55 % of --hero-h tall and anchored at the bottom, so a viewBox y sits at
   (165-y)/300 of the frame above the bottom. The crest oscillates between y=30
   and y=90, mean y=60, i.e. 65-75 % from the top: 68 % is the band .grade-haze
   fogs. Bloom and rays are anchored on the REAL sun (~87 % / 12 %).

   WHY THE NUMBERS ARE HALF THOSE OF THE NIGHT SCENES — every layer of the stack
   is a BLEND, and a blend is relative to what it lands on. On Ocean's near-black
   sky the same intensity buys a whisper; here it buys a stain.
   `--grade-tone-blend: overlay`, not soft-light: soft-light is a gentle S-curve
   around 0.5 and it has almost NO authority in the highlights, which is where a
   pale pink sky lives — at .30 it was indistinguishable from 0 on the capture.
   Rejected the other way round too: `multiply` on a light frame only darkens,
   and it turned the sky into dirty mauve while flattening the sun. */
.hero-scene-2 {
    --hero-horizon: 68%;
    /* Cherry haze reads as the warm dust of a spring afternoon. Half of Ocean's
       .18 and still visible: on a pale sky a haze is ADDING pale to pale. */
    --grade-haze: .09; --grade-haze-color: #ffd9e8;
    /* Halation and veil, not light leaks: on a frame this bright a `screen`
       plate over a near-black texture would add nothing at all. The plate is
       authored light, so it composites in `soft-light` — the one mode that can
       still MODULATE a bright base without either washing or staining it. */
    --grade-plate: .34;
    --grade-plate-src: url("/Images/hero/sakura-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: soft-light;
    --grade-plate-size: 100% 100%;
    /* The sun, and the pool of light it throws on the near ridge below it. */
    --grade-bloom: .40;
    --grade-bloom-x: 87%; --grade-bloom-y: 12%; --grade-bloom-r: 24%; --grade-bloom-color: #ffeec4;
    --grade-bloom2-x: 87%; --grade-bloom2-y: 74%; --grade-bloom2-r: 14%; --grade-bloom2-color: #ffd0b0;
    /* The scene already owns a conic ray fan on the sun; these five slits are
       here for the slow breath and for the shafts that reach the left wing. */
    --grade-rays: .07; --grade-rays-x: 87%; --grade-rays-color: #fff4d8; --grade-rays-anim: gradeRaysDrift;
    --grade-tone: .24; --grade-tone-blend: overlay; --grade-shadow: #5b2a5e; --grade-light: #ffdcb4;
    --grade-fringe: .04;
    /* Half of a night scene's .40-.46: on a pale frame a vignette does not read
       as a lens, it reads as dirt in the corners. */
    --grade-vignette: .20;
    /* `overlay` modulates PROPORTIONALLY to the base, so grain peaks in the
       midtones and is loudest exactly where this frame lives. Ocean's .35 came
       out as visible sand on the sky here. */
    --grade-grain: .13; --grade-grain-anim: gradeGrainShift;
}

/* 3 — Ocean (graded: pilot scene of the doctrine)
   --hero-horizon is MEASURED, not chosen: the waterline is y=220 of a
   `0 0 1200 300` viewBox stretched with preserveAspectRatio="none" over the full
   height, i.e. 220/300 = 73.3 % at every breakpoint. .grade-haze bands on it.
   Bloom and rays are anchored on the REAL moon of the scene: a disc of
   .3 × --hero-h, top .09 × --hero-h, right 14 % → centre at ~83 % / 24 %. */
.hero-scene-3 {
    --hero-horizon: 73.3%;
    /* Haze and tone were dialled back (.34 → .18, .40 → .30) once the water got
       its own texture: at the former intensities the pair washed the whole frame
       to a flat grey-brown fog, which erased the horizon on the moon side and
       swallowed the island ridge. The night is meant to be CLEAR and blue. */
    --grade-haze: .18; --grade-haze-color: #4d82ac;
    /* The only raster of the whole stack, and the only one Ocean pays for: an
       abstract grain/dust/halation plate, near-black background, its warm glow
       authored at ~88 %/14 % so it lands on the moon (83 %/24 %) once stretched.
       `screen` because the plate is black-based — it can only ADD light, never
       darken, so the vector scene keeps every one of its own values. */
    --grade-plate: .16;
    --grade-plate-src: url("/Images/hero/ocean-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: screen;
    --grade-plate-size: 100% 100%;
    --grade-bloom: .50; --grade-bloom-x: 83%; --grade-bloom-y: 24%; --grade-bloom-r: 26%; --grade-bloom-color: #ffe9b0;
    --grade-rays: .09; --grade-rays-x: 83%; --grade-rays-color: #dcf0ff; --grade-rays-anim: gradeRaysDrift;
    --grade-tone: .30; --grade-tone-blend: soft-light; --grade-shadow: #06263f; --grade-light: #ffd9a0;
    --grade-fringe: .05; --grade-vignette: .40;
    /* The plate above carries the LIGHT of the film stock; this carries its
       TEXTURE, at pixel scale. Dialled to the point where it reads as emulsion
       on the sky without being seen as noise — on a night scene very little
       goes a long way, the modulation is relative to a nearly black base. */
    --grade-grain: .35; --grade-grain-anim: gradeGrainShift;
}

/* 4 — Storm (graded)
   --hero-horizon is MEASURED: the waterline is y=220 of a `0 0 1200 300` viewBox
   stretched with preserveAspectRatio="none" over the full height, i.e. 73.3 % at
   every breakpoint — the same number Ocean holds, and the value the boat hangs
   off in hero-animations.css.
   The bloom sits on the LIGHT SLOT under the ceiling (~70 % of the height), which
   is this scene's only steady source. It is deliberately NOT on the lightning:
   an intermittent source cannot carry a static halo — the halo would be there for
   the whole cycle and the flash for two frames of it. */
.hero-scene-4 {
    --hero-horizon: 73.3%;
    /* Rain between the eye and the horizon is exactly what aerial perspective
       describes — but at .24 it fogged sky and sea into ONE grey band, erased the
       waterline and swallowed the boat. Ocean learnt the same lesson at .34. */
    --grade-haze: .13; --grade-haze-color: #6d7a90;
    --grade-plate: .13;
    --grade-plate-src: url("/Images/hero/storm-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: screen;
    --grade-plate-size: 100% 100%;
    --grade-bloom: .22;
    --grade-bloom-x: 30%; --grade-bloom-y: 71%; --grade-bloom-r: 30%; --grade-bloom-color: #b3c2da;
    --grade-bloom2-x: 79%; --grade-bloom2-y: 70%; --grade-bloom2-r: 25%; --grade-bloom2-color: #9fb2cf;
    /* Rain shafts. The one scene where the five slits of .grade-rays are literal
       rather than decorative — and the default 100 deg puts them within a couple
       of degrees of the 12 deg the .rain drops fall at. */
    --grade-rays: .11; --grade-rays-x: 34%; --grade-rays-color: #cdd8ea; --grade-rays-anim: gradeRaysDrift;
    --grade-tone: .28; --grade-tone-blend: soft-light; --grade-shadow: #0b1019; --grade-light: #9db2ce;
    --grade-fringe: .04; --grade-vignette: .46;
    /* The heaviest grain of the three graded scenes: on a storm the texture reads
       as weather rather than as noise, and the frame is dark enough to take it. */
    --grade-grain: .34; --grade-grain-anim: gradeGrainShift;
}

/* 5 — Nebula (graded)
   --hero-horizon is not a landscape line here: deep space has none. It is the
   galactic dust lane at 68 %, the only band .grade-haze can sensibly fog, and
   the scene's stability is proven on its two remarkable OBJECTS instead — the
   black hole (centre 87 % / 28 %) and the planet (centre 13 % / 64 %), both
   anchored by their centre in hero-animations.css.
   Bloom and rays are anchored on those REAL sources — the accretion disc, the
   planet, and the pink cloud core at 10 % / 24 % — never on the centre, which
   the backdrop keeps near-black so the title reads. */
.hero-scene-5 {
    --hero-horizon: 68%;
    /* Barely there, and that is the point: vacuum has no aerial perspective.
       This is the third scene where a haze above ~.2 flattens everything into
       one grey band; on a frame whose middle is deliberately near-black it
       would be the single fastest way to lose the composition. */
    --grade-haze: .08; --grade-haze-color: #4a3570;
    --grade-plate: .15;
    --grade-plate-src: url("/Images/hero/nebula-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: screen;
    --grade-plate-size: 100% 100%;
    /* Three halos on the three real sources. Restrained on purpose: the clouds
       are already `screen`-blended light, and stacking a glow on a glow is how
       a night sky loses its blacks. */
    --grade-bloom: .34;
    --grade-bloom-x: 87%; --grade-bloom-y: 28%; --grade-bloom-r: 17%; --grade-bloom-color: #c9a2ff;
    --grade-bloom2-x: 13%; --grade-bloom2-y: 64%; --grade-bloom2-r: 14%; --grade-bloom2-color: #ff8a4a;
    --grade-bloom3-x: 10%; --grade-bloom3-y: 22%; --grade-bloom3-r: 12%; --grade-bloom3-color: #ff5fae;
    /* Nearly off. There is no atmosphere out here for a light shaft to scatter
       in; the layer is kept for its slow breath on the black hole side only. */
    --grade-rays: .05; --grade-rays-x: 87%; --grade-rays-color: #d8c6ff; --grade-rays-anim: gradeRaysDrift;
    --grade-tone: .26; --grade-tone-blend: soft-light; --grade-shadow: #0b0620; --grade-light: #ff9ad0;
    --grade-fringe: .06; --grade-vignette: .42;
    --grade-grain: .30; --grade-grain-anim: gradeGrainShift;
}

/* 6 — Zen (graded: second DAYLIGHT scene)
   --hero-horizon is the WATERLINE, and it is not merely declared here: the
   .water-surface of hero-animations.css takes its own height from
   calc(100% - var(--hero-horizon)), so the pond edge and the haze band are the
   same number by construction, at every breakpoint.
   Bloom and rays are anchored on the REAL source of the scene, the warm
   blow-out painted into the sky of .hero-anim-zen at 14 % / 4 % — the left
   wing, never the centre, which the grove keeps busy behind the title. */
.hero-scene-6 {
    --hero-horizon: 55%;
    /* Morning mist over the water. Pale green rather than white: a haze takes
       the colour of what it is made of, and this one is the pond breathing. */
    --grade-haze: .10; --grade-haze-color: #dfeccd;
    /* Same plate family as Sakura and for the same reason — a `screen` plate
       over a near-black texture adds nothing to a bright frame. Authored light,
       composited in soft-light: halation and veil, not light leaks. */
    --grade-plate: .32;
    --grade-plate-src: url("/Images/hero/zen-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: soft-light;
    --grade-plate-size: 100% 100%;
    /* The blow-out itself, and its specular column on the water below it. */
    --grade-bloom: .34;
    --grade-bloom-x: 14%; --grade-bloom-y: 10%; --grade-bloom-r: 26%; --grade-bloom-color: #fff2c8;
    --grade-bloom2-x: 14%; --grade-bloom2-y: 62%; --grade-bloom2-r: 13%; --grade-bloom2-color: #ffeec0;
    /* The best-paying effect of the scene: light through a canopy IS shafts.
       Higher than Sakura's .07 — there is a grove here to scatter in. */
    --grade-rays: .13; --grade-rays-x: 14%; --grade-rays-color: #fdf6d0; --grade-rays-anim: gradeRaysDrift;
    /* `overlay`, decided on the render exactly as on Sakura: at soft-light the
       layer had no authority left in the pale sky, which is two thirds of this
       frame; at multiply the grove went to mud and the water lost its sheen. */
    --grade-tone: .22; --grade-tone-blend: overlay; --grade-shadow: #2c4438; --grade-light: #ffeeb4;
    --grade-fringe: .04;
    --grade-vignette: .18;
    /* Even lower than Sakura's .13: this frame's midtones are broader (the
       whole pond sits there) and `overlay` grain peaks precisely there. */
    --grade-grain: .11; --grade-grain-anim: gradeGrainShift;
}

/* 7 — Cyberpunk (graded)
   --hero-horizon is MEASURED: the skyline base — where the three carriers stand
   and the grid floor begins — is pinned at 66 % of --hero-h by construction in
   hero-animations.css, so it lands there at every breakpoint. .grade-haze bands
   on it, which on this scene reads as the city's neon smog.
   Bloom and rays are anchored on the REAL light of the scene, the neon sun,
   whose centre is at 16 % of the width and whose brightest edge is its base, on
   the horizon: 16 % / 62 %. Never on the centre — that is exactly where the sun
   used to sit, straight behind the title. */
.hero-scene-7 {
    --hero-horizon: 66%;
    /* Neon smog, and the one scene of the four where a visible haze is IN the
       subject rather than against it. Still kept under Ocean's .18: the skyline
       silhouette is the composition, and fogging it is losing it. */
    --grade-haze: .14; --grade-haze-color: #7a2a72;
    --grade-plate: .17;
    --grade-plate-src: url("/Images/hero/cyberpunk-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: screen;
    --grade-plate-size: 100% 100%;
    /* Halo of the sun, its bounce on the grid floor just below, and the cool
       city glow the near skyline throws in the opposite wing. */
    --grade-bloom: .46;
    --grade-bloom-x: 16%; --grade-bloom-y: 62%; --grade-bloom-r: 22%; --grade-bloom-color: #ff5c96;
    --grade-bloom2-x: 16%; --grade-bloom2-y: 84%; --grade-bloom2-r: 13%; --grade-bloom2-color: #ff2f80;
    --grade-bloom3-x: 87%; --grade-bloom3-y: 58%; --grade-bloom3-r: 15%; --grade-bloom3-color: #3fa8e8;
    /* The best-paying effect of this scene: a wet neon city is ALL volumetric
       light. Anchored on the sun, so the shafts come from the left wing and die
       out well before the title — the radial mask of .grade-rays sees to that. */
    --grade-rays: .10; --grade-rays-x: 16%; --grade-rays-color: #ffc0dc; --grade-rays-anim: gradeRaysDrift;
    --grade-tone: .30; --grade-tone-blend: soft-light; --grade-shadow: #0a0420; --grade-light: #ff8ac0;
    --grade-fringe: .06; --grade-vignette: .46;
    --grade-grain: .30; --grade-grain-anim: gradeGrainShift;
}

/* 8 — Forest (graded)
   --hero-horizon is MEASURED, on the isolated far plane and on the pixels: per
   column the topmost row the far firs reach, tenth percentile (i.e. the APEX
   line rather than the mean of a sawtooth outline, which sits 15 points lower).
   63.5 / 63.6 / 63.7 / 64.0 % at 400 / 1200 / 1440 / 1920 px — 0.52 points of
   spread. .grade-haze bands on it, which here reads as the mist that hangs
   between the far trunks.
   Bloom and rays are anchored on the REAL light of the scene, the canopy gap
   painted into the sky of .hero-anim-forest at 86 % / 0 % — the right wing,
   never the centre, which the two grove masks keep dim so the title reads. */
.hero-scene-8 {
    --hero-horizon: 64%;
    /* Under Ocean's .18: this frame is dark, and the far plane is pushed back by
       TONE (pale, desaturated firs) rather than by fog. Above ~.24 the whole
       lower half went to one flat grey band — the fifth scene to prove it. */
    --grade-haze: .09; --grade-haze-color: #9ec1a6;
    --grade-plate: .17;
    --grade-plate-src: url("/Images/hero/forest-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: screen;
    --grade-plate-size: 100% 100%;
    /* The gap itself, and the pool of light it throws on the near firs below it. */
    --grade-bloom: .40;
    --grade-bloom-x: 86%; --grade-bloom-y: 4%; --grade-bloom-r: 24%; --grade-bloom-color: #eaffc0;
    --grade-bloom2-x: 84%; --grade-bloom2-y: 46%; --grade-bloom2-r: 14%; --grade-bloom2-color: #cfe89a;
    /* The best-paying effect of the scene, as it was on Zen: light through a
       canopy IS shafts. Higher than Zen's .13 — this grove is denser and darker,
       so the same intensity buys less. */
    --grade-rays: .15; --grade-rays-x: 86%; --grade-rays-color: #eaffcc; --grade-rays-anim: gradeRaysDrift;
    --grade-tone: .28; --grade-tone-blend: soft-light; --grade-shadow: #06200f; --grade-light: #ffeaa8;
    --grade-fringe: .05; --grade-vignette: .42;
    --grade-grain: .30; --grade-grain-anim: gradeGrainShift;
}

/* 9 — Desert (graded: third and BRIGHTEST of the daylight scenes)
   --hero-horizon is MEASURED, on the isolated far plane and on the pixels: the
   mean height of the far crest over every column of the frame, 70.5 / 71.4 /
   71.5 / 71.6 % across the four breakpoints — 1.04 points of spread, all of it
   the one-pixel quantisation of a 140 px hero. .grade-haze bands on it, which
   here is the literal heat haze over the sand.
   Bloom and rays are anchored on the REAL sun, at 15 % / 26 % of the frame:
   the left wing, where the scene moved it FROM dead centre. */
.hero-scene-9 {
    --hero-horizon: 71%;
    /* Warm dust. Lower than Ocean's .18 for the reason Sakura found: on a pale
       sky a haze is adding pale to pale, and this is the palest frame of the ten
       — the sky runs from #ffd080 to #d26428 with nothing dark in it at all. */
    --grade-haze: .10; --grade-haze-color: #f5cfa2;
    /* soft-light, not screen: a `screen` plate over a near-black texture adds
       nothing at all to a frame this bright. Same call as Sakura and Zen. */
    --grade-plate: .32;
    --grade-plate-src: url("/Images/hero/desert-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: soft-light;
    --grade-plate-size: 100% 100%;
    /* The sun, and the light pooling on the dune crests under it. */
    --grade-bloom: .42;
    --grade-bloom-x: 15%; --grade-bloom-y: 26%; --grade-bloom-r: 23%; --grade-bloom-color: #fff0c0;
    --grade-bloom2-x: 15%; --grade-bloom2-y: 76%; --grade-bloom2-r: 13%; --grade-bloom2-color: #ffcf90;
    /* The scene owns a conic fan on the sun already; these five slits are here
       for the slow breath and for the shafts that reach across the dunes. */
    --grade-rays: .08; --grade-rays-x: 15%; --grade-rays-color: #ffeccc; --grade-rays-anim: gradeRaysDrift;
    /* `overlay`, decided on the render exactly as on Sakura and Zen: at
       soft-light the layer had no authority left in the highlights, and this
       frame is nothing BUT highlights; at multiply the sky went to dirty brown
       and the sun flattened into it. */
    --grade-tone: .22; --grade-tone-blend: overlay; --grade-shadow: #5a2c14; --grade-light: #ffdca8;
    --grade-fringe: .04;
    /* Sakura's .20, and for the same reason: on a pale frame a vignette does not
       read as a lens, it reads as dirt in the corners. */
    --grade-vignette: .18;
    /* The lowest of the ten. `overlay` grain peaks in the midtones and this
       frame is one broad midtone from edge to edge — under Zen's .11 it was
       still visible as sand ON the sand. */
    --grade-grain: .10; --grade-grain-anim: gradeGrainShift;
}

/* 10 — Mesh (graded)
   NO --hero-horizon and NO haze, and that is a decision rather than an
   omission: this scene is abstract, it has no depth axis, so there is no band
   for aerial perspective to describe and declaring one would be inventing a
   horizon the composition does not have. (Nebula could still name its dust
   lane; there is no equivalent here.) The scene's stability is proven on its
   remarkable OBJECTS instead — the two hexagons, at 6 % / 24 % and 92 % / 46 %.
   Bloom and rays are anchored on the REAL sources, the cores of the two blob
   planes, all of them in the wings or off the bottom edge. */
.hero-scene-10 {
    --grade-haze: 0; --grade-haze-color: #1c2b4a;
    --grade-plate: .16;
    --grade-plate-src: url("/Images/hero/mesh-plate.webp");   /* root-absolute, see TRAP */
    --grade-plate-blend: screen;
    --grade-plate-size: 100% 100%;
    /* Three halos on the three real cores. Restrained: the blob planes are
       already `screen`-blended light, and stacking a glow on a glow is how a
       dark frame loses its blacks. */
    --grade-bloom: .34;
    --grade-bloom-x: 11%; --grade-bloom-y: 40%; --grade-bloom-r: 18%; --grade-bloom-color: #38c8ff;
    --grade-bloom2-x: 90%; --grade-bloom2-y: 48%; --grade-bloom2-r: 17%; --grade-bloom2-color: #b070ff;
    --grade-bloom3-x: 22%; --grade-bloom3-y: 96%; --grade-bloom3-r: 12%; --grade-bloom3-color: #ff4a92;
    /* Nearly off, for the reason Nebula gives: there is no atmosphere in an
       abstract field for a shaft to scatter in. Kept for the slow breath. */
    --grade-rays: .05; --grade-rays-x: 11%; --grade-rays-color: #cfe8ff; --grade-rays-anim: gradeRaysDrift;
    --grade-tone: .26; --grade-tone-blend: soft-light; --grade-shadow: #050a1e; --grade-light: #7fc4ff;
    --grade-fringe: .07; --grade-vignette: .40;
    --grade-grain: .26; --grade-grain-anim: gradeGrainShift;
}

/* ==========================================================================
   READABILITY HANDOFF — --scrim and --scene-accent, the only two variables of a
   preset that are NOT consumed inside .hero-stage.

   They are declared HERE, on .video-banner, and not in the .hero-scene-N presets
   above, for a structural reason: the title lives in .video-banner-overlay, a
   SIBLING of .hero-stage, and a custom property never travels sideways. Declared
   on the stage the pair was inert by construction — no selector could read it.
   On the common parent both sides see it, the stage included (it inherits), so
   docs/hero-lab, which reads the presets off the live stage, is unaffected.

   :has() is the price of not duplicating ten values into site.css. A browser
   without it falls back to the medians hard-coded in .video-banner-overlay, which
   are a working scrim, never 0.

   --scrim is the alpha at the CENTRE of the elliptical well, so it is a function
   of how bright the scene is UNDER THE TITLE, not of how dark it looks overall:
   the two daylight scenes (Sakura, Desert) and the two pale ones (Zen, Mesh in
   light theme) need the most. Measured white-on-scene contrast at the title box
   drives these numbers — see HISTORY.md for the table.
   ========================================================================== */
.video-banner:has(> .hero-stage.hero-scene-1)  { --scrim: .42; --scene-accent: #6ff0c8; }   /* Aurora */
.video-banner:has(> .hero-stage.hero-scene-2)  { --scrim: .58; --scene-accent: #ff8ab0; }   /* Sakura */
.video-banner:has(> .hero-stage.hero-scene-3)  { --scrim: .44; --scene-accent: #8fd6ff; }   /* Ocean */
.video-banner:has(> .hero-stage.hero-scene-4)  { --scrim: .46; --scene-accent: #a8c4e8; }   /* Storm */
.video-banner:has(> .hero-stage.hero-scene-5)  { --scrim: .45; --scene-accent: #c9a6ff; }   /* Nebula */
.video-banner:has(> .hero-stage.hero-scene-6)  { --scrim: .57; --scene-accent: #8fc466; }   /* Zen */
.video-banner:has(> .hero-stage.hero-scene-7)  { --scrim: .46; --scene-accent: #ff6fae; }   /* Cyberpunk */
.video-banner:has(> .hero-stage.hero-scene-8)  { --scrim: .44; --scene-accent: #8fd07a; }   /* Forest */
.video-banner:has(> .hero-stage.hero-scene-9)  { --scrim: .50; --scene-accent: #ffb066; }   /* Desert */
.video-banner:has(> .hero-stage.hero-scene-10) { --scrim: .44; --scene-accent: #4fd0ff; }   /* Mesh */

/* ==========================================================================
   CAMERA — pointer parallax and dolly. Consumption side only: the pointer is
   published by js/hero.js as two UNITLESS numbers in [-1, 1] on .hero-stage,
   and everything below is CSS. The JS writes ONE element; the depth fan-out is
   var() inheritance, which is what a per-layer write would emulate by hand.

   ADDITIVE BY CONSTRUCTION — the offsets only exist under `.hero-parallax`, a
   class js/hero.js adds when (and only when) it installs its listener. Without
   JS or under 768 px, not one declaration below applies: no `translate` appears
   on any layer, so no layer gains a stacking context it did not already have.
   That is what keeps the prerendered frame — the one the crawlers and the SSR
   capture see — byte-identical to before. (Reduced motion is NOT one of those
   cases any more: by product decision the parallax is installed there too, and
   would only step aside if HONOR_REDUCED_MOTION were flipped back on.)

   `translate`, NOT `transform`. Most of these layers already animate transform
   in @keyframes (islandScroll, gullSoar, sparkleFade, mglintShimmer…); writing
   a transform here would REPLACE the animated one. The autonomous property
   composes instead — translate ∘ rotate ∘ scale ∘ transform — so no scene has
   to be re-wrapped in parallax containers.
   ========================================================================== */
.hero-stage {
    --px: 0;
    --py: 0;
    /* Amplitude tied to --hero-h, so the effect keeps its proportion at all four
       breakpoints: 5 % of the hero height, i.e. ~15 px at 300 px. Parallax that
       is NOTICED as an effect has failed — this buys depth, not movement.
       Y is halved on top of that: a 6.4:1 frame has almost no vertical room, and
       a visible vertical slide would expose the top or bottom edge of a layer. */
    --parallax-amp: calc(var(--hero-h, 300px) * .05);
}

/* Depth per layer. Only the CALIBRATED scenes declare --depth; the others resolve
   to 0 and simply do not move. Ocean (3), Aurora (1) and Storm (4) are calibrated.
   The ordering is the composition's own: stars sit at infinity, the moon and the
   island ridge on the far plane, the water and everything floating ON it share
   ONE value (glints that drifted against their own waves would read as a bug),
   the gulls fly between, and the out-of-focus swell is the near plane.
   The moon and its reflection share a depth on purpose: hero-animations.css
   aligns their centres by construction, and two depths would break that. */
.hero-anim-ocean .star { --depth: .06; }
.hero-anim-ocean .moon,
.hero-anim-ocean .moon-reflection,
.hero-anim-ocean .island-layer { --depth: .12; }
.hero-anim-ocean svg.bg,
.hero-anim-ocean .sparkle,
.hero-anim-ocean .mglint { --depth: .30; }
.hero-anim-ocean .gull { --depth: .45; }
.hero-anim-ocean .swell-fg { --depth: .60; }

.hero-stage.hero-parallax .hero-anim-ocean .star,
.hero-stage.hero-parallax .hero-anim-ocean .moon,
.hero-stage.hero-parallax .hero-anim-ocean .moon-reflection,
.hero-stage.hero-parallax .hero-anim-ocean .island-layer,
.hero-stage.hero-parallax .hero-anim-ocean .sparkle,
.hero-stage.hero-parallax .hero-anim-ocean .mglint,
.hero-stage.hero-parallax .hero-anim-ocean .gull {
    translate: calc(var(--px, 0) * var(--depth, 0) * var(--parallax-amp))
               calc(var(--py, 0) * var(--depth, 0) * var(--parallax-amp) * .5);
}

/* The two layers that ALREADY own `translate` — for their own centring, not for
   an animation (svg.bg and .swell-fg are re-framed from 50 % with a -50 %
   shift). The parallax offset is ADDED to it; dropping the -50 % here would
   shove the whole composition half a frame to the right. */
.hero-stage.hero-parallax .hero-anim-ocean svg.bg,
.hero-stage.hero-parallax .hero-anim-ocean .swell-fg {
    translate: calc(-50% + var(--px, 0) * var(--depth, 0) * var(--parallax-amp))
               calc(var(--py, 0) * var(--depth, 0) * var(--parallax-amp) * .5);
}

/* Aurora. Same ordering logic as Ocean: stars at infinity, the two curtains just
   in front of them (they are sky, not scenery), the three ground planes receding
   toward the viewer, and the snow — which falls between the eye and everything —
   as the near plane. The curtains and the ranges already animate `transform`
   (auroraDrift*, mountainScroll); `translate` composes with it instead of
   replacing it, which is the whole reason this file uses the autonomous property. */
.hero-anim-aurora .star,
.hero-anim-aurora .shooting-star { --depth: .06; }
.hero-anim-aurora .aurora-curtain { --depth: .10; }
.hero-anim-aurora .m-far { --depth: .16; }
.hero-anim-aurora .m-mid { --depth: .26; }
.hero-anim-aurora .m-near { --depth: .36; }
.hero-anim-aurora .snowflake { --depth: .50; }

/* Storm. The sea and the boat share ONE value on purpose: a hull that drifted
   against its own waves would read as a bug, exactly as Ocean's glints would. */
.hero-anim-storm .cl-far { --depth: .10; }
.hero-anim-storm .cl-mid { --depth: .20; }
.hero-anim-storm .cl-near { --depth: .30; }
.hero-anim-storm svg.bg,
.hero-anim-storm .boat { --depth: .26; }
.hero-anim-storm .storm-gull { --depth: .40; }
.hero-anim-storm .rain { --depth: .55; }

.hero-stage.hero-parallax .hero-anim-aurora .star,
.hero-stage.hero-parallax .hero-anim-aurora .shooting-star,
.hero-stage.hero-parallax .hero-anim-aurora .aurora-curtain,
.hero-stage.hero-parallax .hero-anim-aurora .mountain-layer,
.hero-stage.hero-parallax .hero-anim-aurora .snowflake,
.hero-stage.hero-parallax .hero-anim-storm .cloud-layer,
.hero-stage.hero-parallax .hero-anim-storm .boat,
.hero-stage.hero-parallax .hero-anim-storm .storm-gull,
.hero-stage.hero-parallax .hero-anim-storm .rain {
    translate: calc(var(--px, 0) * var(--depth, 0) * var(--parallax-amp))
               calc(var(--py, 0) * var(--depth, 0) * var(--parallax-amp) * .5);
}

/* Storm's svg.bg ALREADY owns `translate` for its own centring (-50 %), like
   Ocean's: the offset is added to it, never substituted for it. */
.hero-stage.hero-parallax .hero-anim-storm svg.bg {
    translate: calc(-50% + var(--px, 0) * var(--depth, 0) * var(--parallax-amp))
               calc(var(--py, 0) * var(--depth, 0) * var(--parallax-amp) * .5);
}

/* Nebula. There is no ground plane out here, so the ordering is the optical
   one: the star field is at infinity, the coloured clouds are the far volume,
   the black hole and the planet are the two SUBJECTS, and the comets pass in
   front of everything. .nebula-cloud animates transform (nebulaSwirl), .planet
   animates it too (planetDrift), and .comet rides an offset-path — `translate`
   composes with all three, which is why this file never writes `transform`. */
.hero-anim-nebula .star { --depth: .04; }
.hero-anim-nebula .nebula-cloud { --depth: .09; }
.hero-anim-nebula .blackhole { --depth: .16; }
.hero-anim-nebula .planet { --depth: .22; }
.hero-anim-nebula .comet { --depth: .34; }

/* Cyberpunk. The three skyline planes ARE the depth of the scene, so they take
   the ordering straight from their scroll speeds (120 s / 70 s / 45 s); the sun
   sits behind all of them, the aircraft fly between the city and the eye, and
   the rain is the near plane. .grid-floor is deliberately absent: it is pinned
   to the bottom edge of the frame, and a vertical offset there would open a bare
   strip under it. The sun's own `transform: translateX(-50%)` is untouched —
   `translate` and `transform` compose, they do not replace each other. */
.hero-anim-cyberpunk .neon-sun { --depth: .06; }
.hero-anim-cyberpunk .skyline-far { --depth: .12; }
.hero-anim-cyberpunk .skyline-mid { --depth: .22; }
.hero-anim-cyberpunk .skyline-near { --depth: .32; }
.hero-anim-cyberpunk .hovercraft { --depth: .46; }
.hero-anim-cyberpunk .cyber-rain { --depth: .60; }

/* Sakura. The ordering is the scene's own optics: the sun and its shaft fan are
   the sky, the two ridges recede toward the viewer, the torii stands on the
   near one, and the petals — which fall BETWEEN the eye and everything — are
   the near plane with the butterflies just behind them. The branches are
   deliberately absent: they are rooted in the two bottom corners, and a
   horizontal offset there would open a bare strip along the frame edge.
   `.sun-rays` and the two ridges already animate `transform` (raysSpin,
   sakuraMountainScroll); `translate` composes with it instead of replacing it. */
.hero-anim-sakura .sakura-sun,
.hero-anim-sakura .sun-rays { --depth: .05; }
.hero-anim-sakura .sm-far { --depth: .13; }
.hero-anim-sakura .sm-near { --depth: .24; }
.hero-anim-sakura .sakura-torii { --depth: .24; }
.hero-anim-sakura .butterfly { --depth: .42; }
.hero-anim-sakura .petal { --depth: .58; }

/* Zen. The three groves take their ordering straight from their scroll speeds
   (300 s / 175 s / 90 s); the pond and everything floating ON it share ONE
   value, because a lotus that drifted against its own water would read as a
   bug — the same rule Ocean applies to its glints. .ripple is absent on
   purpose: it owns `translate: -50% -50%` for its own centring, and the
   parallax offset would replace it and shove every ring off its anchor. */
.hero-anim-zen .b-far { --depth: .10; }
.hero-anim-zen .b-mid { --depth: .20; }
.hero-anim-zen .water-surface,
.hero-anim-zen .lotus,
.hero-anim-zen .koi { --depth: .30; }
.hero-anim-zen .firefly { --depth: .44; }
.hero-anim-zen .leaf-twirl { --depth: .52; }
.hero-anim-zen .b-near { --depth: .60; }
.hero-anim-zen svg.bg { --depth: .30; }

/* Forest. The three groves take their ordering straight from their scroll
   speeds (180 s / 100 s / 55 s); the pollen and the fireflies drift between the
   eye and the grove, so they are the near plane. .god-rays and .mist are
   deliberately absent: both are full-box layers pinned to an edge, and a
   horizontal offset there would expose a bare strip — the same reason
   Cyberpunk leaves its grid floor out. The three .forest-wrap are static
   frames, so the offset goes on the SCROLLING <svg> inside them, which already
   animates `transform` (forestScroll): `translate` composes with it. */
.hero-anim-forest .f-far  { --depth: .10; }
.hero-anim-forest .f-mid  { --depth: .22; }
.hero-anim-forest .f-near { --depth: .34; }
.hero-anim-forest .pollen-bz { --depth: .48; }
.hero-anim-forest .firefly-f { --depth: .56; }

/* Desert. The sun sits at infinity, the three dune planes follow their scroll
   speeds (240 s / 150 s / 80 s), the vultures fly between the ridge and the eye
   and the grains are the near plane. .sun-rays-d and .sand-storm are left out:
   the first already owns `translate: -50% -50%` for its own centring and the
   offset would replace it, and the second is a full-box layer. */
.hero-anim-desert .sun-big { --depth: .05; }
.hero-anim-desert .d-far   { --depth: .12; }
.hero-anim-desert .d-mid   { --depth: .22; }
.hero-anim-desert .d-near  { --depth: .34; }
.hero-anim-desert .vulture { --depth: .44; }
.hero-anim-desert .sand-particle { --depth: .58; }

/* Mesh. The optical ordering of an abstract field: the two blob planes are the
   volume behind everything, the network is the structure, the hexagons are the
   two SUBJECTS and the pulses pass in front. .tech-line is absent on purpose —
   each one is pinned to left:0 and an offset would open a gap at the edge. */
.hero-anim-mesh .blob-plane { --depth: .06; }
.hero-anim-mesh .tech-dot   { --depth: .20; }
.hero-anim-mesh .hex-rotate,
.hero-anim-mesh .hex-rotate-2 { --depth: .28; }
.hero-anim-mesh .pulse      { --depth: .42; }

.hero-stage.hero-parallax .hero-anim-forest .forest-layer,
.hero-stage.hero-parallax .hero-anim-forest .pollen-bz,
.hero-stage.hero-parallax .hero-anim-forest .firefly-f,
/* NOTE: .hero-anim-desert .sun-big is deliberately NOT in this group — it needs
   the -50 % centring variant and is declared in the block below. Re-adding it
   here would be dead weight today (equal specificity, later rule wins) and an
   actual half-width shift of the sun the day this file is re-ordered. */
.hero-stage.hero-parallax .hero-anim-desert .dune-layer,
.hero-stage.hero-parallax .hero-anim-desert .vulture,
.hero-stage.hero-parallax .hero-anim-desert .sand-particle,
.hero-stage.hero-parallax .hero-anim-mesh .blob-plane,
.hero-stage.hero-parallax .hero-anim-mesh .tech-dot,
.hero-stage.hero-parallax .hero-anim-mesh .pulse {
    translate: calc(var(--px, 0) * var(--depth, 0) * var(--parallax-amp))
               calc(var(--py, 0) * var(--depth, 0) * var(--parallax-amp) * .5);
}

/* .sun-big and the two hexagons ALREADY own `translate` for their own centring
   (-50 % on x), like Ocean's water: the offset is ADDED to it, never
   substituted for it. Dropping the -50 % here would shove each of them half its
   own width to the right — and for the hexagons that is the very anchor the
   stability of scene 10 is measured on. */
.hero-stage.hero-parallax .hero-anim-desert .sun-big,
.hero-stage.hero-parallax .hero-anim-mesh .hex-rotate,
.hero-stage.hero-parallax .hero-anim-mesh .hex-rotate-2 {
    translate: calc(-50% + var(--px, 0) * var(--depth, 0) * var(--parallax-amp))
               calc(var(--py, 0) * var(--depth, 0) * var(--parallax-amp) * .5);
}

/* The stones ALREADY own `translate` for their own centring (-50 %), like
   Ocean's water: the offset is added to it, never substituted for it — and the
   value has to match the water they lie on, or they would float off it. */
.hero-stage.hero-parallax .hero-anim-zen svg.bg {
    translate: calc(-50% + var(--px, 0) * var(--depth, 0) * var(--parallax-amp))
               calc(var(--py, 0) * var(--depth, 0) * var(--parallax-amp) * .5);
}

.hero-stage.hero-parallax .hero-anim-sakura .sakura-sun,
.hero-stage.hero-parallax .hero-anim-sakura .sun-rays,
.hero-stage.hero-parallax .hero-anim-sakura .sakura-mountains,
.hero-stage.hero-parallax .hero-anim-sakura .sakura-torii,
.hero-stage.hero-parallax .hero-anim-sakura .butterfly,
.hero-stage.hero-parallax .hero-anim-sakura .petal,
.hero-stage.hero-parallax .hero-anim-zen .bamboo-layer,
.hero-stage.hero-parallax .hero-anim-zen .water-surface,
.hero-stage.hero-parallax .hero-anim-zen .lotus,
.hero-stage.hero-parallax .hero-anim-zen .koi,
.hero-stage.hero-parallax .hero-anim-zen .firefly,
.hero-stage.hero-parallax .hero-anim-zen .leaf-twirl,
.hero-stage.hero-parallax .hero-anim-nebula .star,
.hero-stage.hero-parallax .hero-anim-nebula .nebula-cloud,
.hero-stage.hero-parallax .hero-anim-nebula .blackhole,
.hero-stage.hero-parallax .hero-anim-nebula .planet,
.hero-stage.hero-parallax .hero-anim-nebula .comet,
.hero-stage.hero-parallax .hero-anim-cyberpunk .neon-sun,
.hero-stage.hero-parallax .hero-anim-cyberpunk .skyline-layer,
.hero-stage.hero-parallax .hero-anim-cyberpunk .hovercraft,
.hero-stage.hero-parallax .hero-anim-cyberpunk .cyber-rain {
    translate: calc(var(--px, 0) * var(--depth, 0) * var(--parallax-amp))
               calc(var(--py, 0) * var(--depth, 0) * var(--parallax-amp) * .5);
}

/* DOLLY — the camera move, in pure CSS: it is a fixed loop with no input, so a
   frame of JavaScript would buy nothing. It rides the autonomous `scale`, and an
   off-centre transform-origin turns that ONE property into zoom AND drift (a
   scale about a non-centred origin translates), so nothing else has to animate.

   On the near plane ONLY, and that is the whole design:
   - never .hero-stage nor .hero-anim — continuously scaling a subtree of 100+
     animated elements re-rasterises all of it on every frame;
   - never svg.bg — its waterline is MEASURED at 73.3 % and --grade-haze,
     .moon-reflection and .mglint are all calibrated on it; scaling the water
     would walk the horizon away from four things that follow it by number;
   - .swell-fg is the one plane that can take it: static content (so it
     rasterises once and only the compositor transform varies), already blurred,
     already overscanned by 4 %, and nothing is calibrated against its edges.
   Because it is a CSS animation on a descendant of .hero-anim, the (now disarmed)
   prefers-reduced-motion rule of hero-animations.css is what would stop it again,
   the day HONOR_REDUCED_MOTION is flipped back on in js/hero.js. */
.hero-anim-ocean .swell-fg {
    transform-origin: 38% 100%;
    animation: heroDolly 40s ease-in-out infinite alternate;
}

@keyframes heroDolly {
    from { scale: 1; }
    to   { scale: 1.06; }
}

@media (max-width: 768px) {
    /* Only haze, plate, tone and vignette survive: the others are pure
       decoration and each full-box layer costs 9.2 MB of GPU memory at DPR 2.
       Grain goes too — it is the layer that gains the least on a small screen
       (a DPR 2 phone doubles the tile, so the texture is half as fine) for the
       cost of a promoted, animated full-box layer. */
    .hero-stage > .grade-bloom,
    .hero-stage > .grade-rays,
    .hero-stage > .grade-fringe,
    .hero-stage > .grade-grain { display: none; }

    /* The parallax already switches itself off here (js/hero.js never installs
       its listener under 768 px, so `.hero-parallax` is never added). The dolly
       is CSS and would keep running: a phone gets neither. */
    .hero-anim-ocean .swell-fg { animation: none; }
}

/* PRODUCT DECISION — see the same note in hero-animations.css: the hero animates for
   every visitor, reduced-motion included. The rule below is intact but DISARMED, gated
   on `.hero-honor-reduced-motion` (added by js/hero.js only when HONOR_REDUCED_MOTION is
   true, currently false). Not an oversight, and reversible by that single flag. */
@media (prefers-reduced-motion: reduce) {
    /* The rest of the stack is static; the rays drift and the grain shifts.
       The grain layer is kept — frozen, it is still film texture, and the
       texture is not the motion. */
    .hero-honor-reduced-motion .hero-stage > .grade-rays,
    .hero-honor-reduced-motion .hero-stage > .grade-grain {
        animation: none !important;
        transform: none !important;
    }
}
