/* =========================================================
   AC-v2 Section: The Ends of the Earth (#the-ends-of-the-earth)
   Spec: AC-v2-site-architecture-190426.md §5.8
   Golden master: AC-v2/archive/vis-v2-220426-teote-GOLDEN-MASTER.teote-preview-fluid-overlays.html
   Step-function lockdown: AC-v2/sandbox/vis-v2-230426-teote-startend-5anchor-lockdown.teote-startend-230426.html
   Integrated: 22.04.26  /  Step-function model ported: 23.04.26

   Architecture: sticky parallax stage inside a 300vh track. UI chrome
   (logo, breadcrumb, tabs, card, credit) is absolutely positioned inside
   the sticky stage-wrap using cqh tokens from a three-anchor fluid system
   (320 / 576 / 1440). Layer parallax (Malus, Toby+Molly, background) runs
   off a four-anchor step-function with a pass-through 400 anchor and 8px
   crossfade bands at 320/400/576, locking each composition at its anchor
   rather than drifting linearly on resize.

   CSS prefix: teote-  /  section class: section--teote (unused at root;
   the track + stage-wrap pattern replaces the standard section shell).
   Z-index registry: all TEotE z-indexes are internal to .teote-stage-wrap
   (isolation: isolate), so they do not collide with the nav (z: 100) or
   any other root-stacking-context element.
   ========================================================= */

/* Scroll-driven CSS animations attempt (10.05.26) was reverted.
   Needed `timeline-scope: --teote-vt` on :root for the descendant
   view-timeline to be visible from the root animation host. Without
   it, the animation never ran, AND the JS fallback was skipped via
   feature-detection, so --scroll-progress stayed at 0 → page froze.
   Revisit in isolation; for now JS continues to drive scroll-progress. */

/* ---------- Fluid progress systems ----------
   TWO progress systems run side-by-side:

   A. Three-anchor fluid (320 / 576 / 1440), used ONLY by the chrome tokens
      (logo, breadcrumb, tabs, card, credit). Continuous across the range.

   B. Four-anchor step-function (320 / 576 / 991 / 1440) + pass-through 400.
      Used by the start/end parallax blend for Malus / TM / BG. Locks the
      composition at each anchor and crossfades in a narrow 8px band above
      each lower anchor, so mobile resize feels like a snap between
      locked compositions rather than a slow drift. 991 → 1440 stays linear
      (desktop stretch). Ported from sandbox/teote-startend-230426.html
      (locked 23.04.26). */
:root {
  /* ---- Three-anchor fluid (chrome positioning / sizing) ---- */
  --mobile-progress: clamp(0, calc((100vw - 320px) / (400px - 320px)), 1);
  --progress:        clamp(0, calc((100vw - 576px) / (1440px - 576px)), 1);

  /* ---- Four-anchor step-function (layer start/end blend) ----
     Each vpp-*-* is a clamped ramp across its range. Stacked additive
     deltas weighted by these progresses give a piecewise-linear blend
     that snaps at each anchor. Below 320 the first var is 0; above 1440
     the last is 1. 991 → 1440 uses the full range as a linear stretch
     (no narrow band) because desktop drift reads fine. */
  --vpp-320-400:  clamp(0, calc((100vw - 320px) / 8px), 1);     /* crossfade 320 → 328 */
  --vpp-400-576:  clamp(0, calc((100vw - 400px) / 8px), 1);     /* crossfade 400 → 408 */
  --vpp-576-991:  clamp(0, calc((100vw - 720px) / 8px), 1);     /* crossfade 720 → 728 (shifted from 576 on 23.04.26; holds 576 composition up to 720 to prevent L/R clipping at intermediate widths) */
  --vpp-991-1440: clamp(0, calc((100vw - 991px) / (1440px - 991px)), 1);

  /* ---- UI chrome positioning ----
     Logo + author credit are pinned to fixed pixel values (was viewport-
     scaled cqh and progress-blended px before the 10.05.26 graduation
     from the callouts sandbox). The callout stack sits below them with
     a 60px gap to the headline text-top. Below 576 the logo reverts to
     260px (see media query). */
  --logo-author-space: 30px;
  --logo-group-height: calc(var(--logo-size) / 2.1 + var(--logo-author-space));
  --logo-y:            175px;   /* 15.05.26: shifted up 25px per Caoimhín (was 200px). Carries logo-group, callout-stack, narrow-height credit. */
  --logo-size:         312px;
  --logo-x:            50%;

  /* Breadcrumb clears the fixed nav. --nav-height from tokens.css
     (100px desktop, 76px mobile); 12px gap below nav bottom. */
  --breadcrumb-y: calc(var(--nav-height) + 12px);

  /* ---- Central callout stack ----
     19.05.26 (pm v2): top + bottom text parts unified to the same
     content width via the secondary blockquote margin reset (below);
     stack + glass then bumped +10% per user (735 -> 808, 765 -> 842). */
  --callout-gap:           60px;   /* author-credit-bottom to headline-text-top */
  --callout-stack-width:   min(808px, calc(100vw - 32px));  /* r34 21.05.26: cap at viewport-32px so the callout cards never clip the L/R edges between 576 and 808px (was a fixed 808px, which overflowed). 16px gutter each side at all widths. */
  --callout-text-pad-x:    120px;  /* L/R padding inside headline + secondary; ramps 30 -> 10 across 320-575.98 */

  /* ---- Toby eye glow REMOVED 12.05.26 ----
     Calibration values (per-BP positions, colours, blend mode,
     flicker var) preserved in
     archive/fix-v3-120526-pre-toby-eye-removal.section-teote.css
     if the layer is ever restored. */

  /* ---- Malus atmospheric tunables ----
     The eye-anchor system (face-pct positions, per-BP eye coords,
     scale, anchor-w/h, switch thresholds) was removed 12.05.26
     along with the .malus-eye-anchor DOM and its JS rAF tracker.
     Calibration preserved in
     archive/fix-v3-120526-pre-malus-eye-cleanup.section-teote.css.
     The two vars below remain in use: vortex-flicker multiplies
     into --malus-opacity, stretch-max drives the scaleY transform. */
  --malus-vortex-flicker: 1;
  --malus-stretch-max:    0.15;

  /* =========================================================
     START / END POSITIONING TOKENS (23.04.26 refactor)
     =========================================================
     Each layer has eight calibration anchors:
       start-320  start-576  start-991  start-1440   composition at scroll=0
       end-320    end-576    end-991    end-1440     composition at scroll=1
     Blended by the four piecewise vpp progresses (320→400, 400→576,
     576→991, 991→1440) and by --scroll-progress (0 at stage entry,
     1 at stage fully exited).

     To fix a clearance problem at scroll=1, edit the end-* values.
     To change the opening composition, edit the start-* values.
     400 anchor is pass-through by default (uses 320 values); override
     the `var(--*-320)` lines below if you want a distinct 400 comp.
     ---- MALUS (vapour atmospheric layer) ---- */
  --malus-start-x-320:   60.98%;
  --malus-start-x-576:   70.46%;
  --malus-start-x-991:   68.95%;
  --malus-start-x-1440:  69.47%;
  --malus-start-y-320:   7.45%;
  --malus-start-y-576:   4.77%;
  --malus-start-y-991:   -4.00%;
  --malus-start-y-1440:  4.47%;
  --malus-start-w-320:   56.5%;
  --malus-start-w-576:   48%;
  --malus-start-w-991:   56%;
  --malus-start-w-1440:  43.5%;
  --malus-end-x-320:     58.67%;
  --malus-end-x-576:     72.23%;
  --malus-end-x-991:     71.41%;
  --malus-end-x-1440:    71.23%;
  --malus-end-y-320:     26.71%;
  --malus-end-y-576:     15.00%;
  --malus-end-y-991:     19.05%;
  --malus-end-y-1440:    11.48%;
  --malus-end-w-320:     48.5%;
  --malus-end-w-576:     58%;
  --malus-end-w-991:     51%;
  --malus-end-w-1440:    59.5%;

  /* Scroll-driven cloud opacity: 0.30 base rises to 0.40 by scroll-end,
     modulated by the flicker loop (multiplier 0.84 -> 1). */
  --malus-opacity: calc((0.30 + var(--scroll-progress, 0) * 0.10) * var(--malus-vortex-flicker, 1));
  --malus-blur:    5px;
  --malus-blend:   lighten;
  --malus-drift-x: 0px;
  --malus-drift-y: 0px;

  /* ---- TOBY + MOLLY (foreground figures) ---- */
  --tm-start-x-320:    50.78%;
  --tm-start-x-576:    37%;
  --tm-start-x-991:    25.96%;
  --tm-start-x-1440:   26.67%;
  --tm-start-y-320:    43.84%;  /* 23.04.26 nudge: ~20px up at 320×700 canonical (canvas h = 700px → 2.86%). 400 pass-through inherits. */
  --tm-start-y-576:    25.85%;
  --tm-start-y-991:    33.2%;   /* 21.05.26 r33: desktop start lower (parallax sandbox tune; was 25.56%) */
  --tm-start-y-1440:   32.96%;  /* 21.05.26 r33: desktop start lower (parallax sandbox tune; was 24.51%) */
  --tm-start-w-320:    41%;
  --tm-start-w-576:    42.5%;
  --tm-start-w-991:    39.5%;
  --tm-start-w-1440:   36%;
  --tm-end-x-320:      29.77%;
  --tm-end-x-576:      35.5%;
  --tm-end-x-991:      27.57%;
  --tm-end-x-1440:     28.35%;
  --tm-end-y-320:      46.24%;  /* 23.04.26 nudge: ~20px up at 320×700 canonical (canvas h = 700px → 2.86%). 400 pass-through inherits. */
  --tm-end-y-576:      37.54%;
  --tm-end-y-991:      37.65%;
  --tm-end-y-1440:     37.73%;
  --tm-end-w-320:      41%;
  --tm-end-w-576:      48%;
  --tm-end-w-991:      46%;
  --tm-end-w-1440:     44%;

  --tm-opacity: 1;

  /* ---- BACKGROUND (translate + scale over object-fit:cover) ----
     Motion is applied via transform on the img (translate + scale) on
     top of object-position. Tx/ty are in cqw/cqh so they reference the
     stage container. Scale is unitless. */
  --bg-x: 50%;   /* object-position x (fixed) */
  --bg-y: 50%;   /* object-position y (fixed) */

  --bg-start-tx-320:      0cqw;
  --bg-start-tx-576:      -12.5cqw;
  --bg-start-tx-991:      -2cqw;
  --bg-start-tx-1440:     -0.5cqw;
  --bg-start-ty-320:      -6.5cqh;
  --bg-start-ty-576:      -21cqh;
  --bg-start-ty-991:      -11cqh;   /* 21.05.26 r33: desktop start lower (parallax sandbox tune; was -17.5cqh) */
  --bg-start-ty-1440:     -7.5cqh;  /* 21.05.26 r33: desktop start lower (parallax sandbox tune; was -14cqh) */
  --bg-start-scale-320:   1.21;
  --bg-start-scale-576:   1.32;
  --bg-start-scale-991:   1.21;
  --bg-start-scale-1440:  1.1;
  --bg-end-tx-320:        -16.5cqw;
  --bg-end-tx-576:        -9cqw;
  --bg-end-tx-991:        0.5cqw;
  --bg-end-tx-1440:       0.5cqw;
  --bg-end-ty-320:        1cqh;
  --bg-end-ty-576:        2.5cqh;
  --bg-end-ty-991:        4cqh;
  --bg-end-ty-1440:       4.5cqh;
  --bg-end-scale-320:     1.2;
  --bg-end-scale-576:     1.23;
  --bg-end-scale-991:     1.17;
  --bg-end-scale-1440:    1.15;

  /* ---- 400 pass-through anchor ----------------------------------
     Pass-through of the 320 values, so the 400 anchor exists only so
     the crossfade band can sit at 400 (phones ≤400 stay locked on the
     320 composition, 400→408 is the real crossfade into the 576 comp).
     Override any of these with a literal value to give 400 its own
     distinct composition. */
  --malus-start-x-400:  var(--malus-start-x-320);
  --malus-start-y-400:  var(--malus-start-y-320);
  --malus-start-w-400:  var(--malus-start-w-320);
  --malus-end-x-400:    var(--malus-end-x-320);
  --malus-end-y-400:    var(--malus-end-y-320);
  --malus-end-w-400:    var(--malus-end-w-320);
  --tm-start-x-400:     var(--tm-start-x-320);
  --tm-start-y-400:     var(--tm-start-y-320);
  --tm-start-w-400:     var(--tm-start-w-320);
  --tm-end-x-400:       var(--tm-end-x-320);
  --tm-end-y-400:       var(--tm-end-y-320);
  --tm-end-w-400:       var(--tm-end-w-320);
  --bg-start-tx-400:    var(--bg-start-tx-320);
  --bg-start-ty-400:    var(--bg-start-ty-320);
  --bg-start-scale-400: var(--bg-start-scale-320);
  --bg-end-tx-400:      var(--bg-end-tx-320);
  --bg-end-ty-400:      var(--bg-end-ty-320);
  --bg-end-scale-400:   var(--bg-end-scale-320);

  /* ---------- Blended values ----------
     Two-step blend per property:
       (1) viewport: stacked additive across the four anchors, weighted
           by the three 8px crossfade vpp vars + the linear 991→1440 vpp;
       (2) scroll:   linear start→end driven by --scroll-progress.
     Consumers read the final --malus-x / --tm-x / --bg-tx etc. as before. */

  /* Malus vp-blended start + end */
  --malus-start-x: calc(var(--malus-start-x-320) + var(--vpp-320-400) * (var(--malus-start-x-400) - var(--malus-start-x-320)) + var(--vpp-400-576) * (var(--malus-start-x-576) - var(--malus-start-x-400)) + var(--vpp-576-991) * (var(--malus-start-x-991) - var(--malus-start-x-576)) + var(--vpp-991-1440) * (var(--malus-start-x-1440) - var(--malus-start-x-991)));
  --malus-start-y: calc(var(--malus-start-y-320) + var(--vpp-320-400) * (var(--malus-start-y-400) - var(--malus-start-y-320)) + var(--vpp-400-576) * (var(--malus-start-y-576) - var(--malus-start-y-400)) + var(--vpp-576-991) * (var(--malus-start-y-991) - var(--malus-start-y-576)) + var(--vpp-991-1440) * (var(--malus-start-y-1440) - var(--malus-start-y-991)));
  --malus-start-w: calc(var(--malus-start-w-320) + var(--vpp-320-400) * (var(--malus-start-w-400) - var(--malus-start-w-320)) + var(--vpp-400-576) * (var(--malus-start-w-576) - var(--malus-start-w-400)) + var(--vpp-576-991) * (var(--malus-start-w-991) - var(--malus-start-w-576)) + var(--vpp-991-1440) * (var(--malus-start-w-1440) - var(--malus-start-w-991)));
  --malus-end-x:   calc(var(--malus-end-x-320) + var(--vpp-320-400) * (var(--malus-end-x-400) - var(--malus-end-x-320)) + var(--vpp-400-576) * (var(--malus-end-x-576) - var(--malus-end-x-400)) + var(--vpp-576-991) * (var(--malus-end-x-991) - var(--malus-end-x-576)) + var(--vpp-991-1440) * (var(--malus-end-x-1440) - var(--malus-end-x-991)));
  --malus-end-y:   calc(var(--malus-end-y-320) + var(--vpp-320-400) * (var(--malus-end-y-400) - var(--malus-end-y-320)) + var(--vpp-400-576) * (var(--malus-end-y-576) - var(--malus-end-y-400)) + var(--vpp-576-991) * (var(--malus-end-y-991) - var(--malus-end-y-576)) + var(--vpp-991-1440) * (var(--malus-end-y-1440) - var(--malus-end-y-991)));
  --malus-end-w:   calc(var(--malus-end-w-320) + var(--vpp-320-400) * (var(--malus-end-w-400) - var(--malus-end-w-320)) + var(--vpp-400-576) * (var(--malus-end-w-576) - var(--malus-end-w-400)) + var(--vpp-576-991) * (var(--malus-end-w-991) - var(--malus-end-w-576)) + var(--vpp-991-1440) * (var(--malus-end-w-1440) - var(--malus-end-w-991)));
  --malus-x: calc(var(--malus-start-x) + var(--scroll-progress) * (var(--malus-end-x) - var(--malus-start-x)));
  --malus-y: calc(var(--malus-start-y) + var(--scroll-progress) * (var(--malus-end-y) - var(--malus-start-y)));
  --malus-w: calc(var(--malus-start-w) + var(--scroll-progress) * (var(--malus-end-w) - var(--malus-start-w)));

  /* TM vp-blended start + end */
  --tm-start-x: calc(var(--tm-start-x-320) + var(--vpp-320-400) * (var(--tm-start-x-400) - var(--tm-start-x-320)) + var(--vpp-400-576) * (var(--tm-start-x-576) - var(--tm-start-x-400)) + var(--vpp-576-991) * (var(--tm-start-x-991) - var(--tm-start-x-576)) + var(--vpp-991-1440) * (var(--tm-start-x-1440) - var(--tm-start-x-991)));
  --tm-start-y: calc(var(--tm-start-y-320) + var(--vpp-320-400) * (var(--tm-start-y-400) - var(--tm-start-y-320)) + var(--vpp-400-576) * (var(--tm-start-y-576) - var(--tm-start-y-400)) + var(--vpp-576-991) * (var(--tm-start-y-991) - var(--tm-start-y-576)) + var(--vpp-991-1440) * (var(--tm-start-y-1440) - var(--tm-start-y-991)));
  --tm-start-w: calc(var(--tm-start-w-320) + var(--vpp-320-400) * (var(--tm-start-w-400) - var(--tm-start-w-320)) + var(--vpp-400-576) * (var(--tm-start-w-576) - var(--tm-start-w-400)) + var(--vpp-576-991) * (var(--tm-start-w-991) - var(--tm-start-w-576)) + var(--vpp-991-1440) * (var(--tm-start-w-1440) - var(--tm-start-w-991)));
  --tm-end-x:   calc(var(--tm-end-x-320) + var(--vpp-320-400) * (var(--tm-end-x-400) - var(--tm-end-x-320)) + var(--vpp-400-576) * (var(--tm-end-x-576) - var(--tm-end-x-400)) + var(--vpp-576-991) * (var(--tm-end-x-991) - var(--tm-end-x-576)) + var(--vpp-991-1440) * (var(--tm-end-x-1440) - var(--tm-end-x-991)));
  --tm-end-y:   calc(var(--tm-end-y-320) + var(--vpp-320-400) * (var(--tm-end-y-400) - var(--tm-end-y-320)) + var(--vpp-400-576) * (var(--tm-end-y-576) - var(--tm-end-y-400)) + var(--vpp-576-991) * (var(--tm-end-y-991) - var(--tm-end-y-576)) + var(--vpp-991-1440) * (var(--tm-end-y-1440) - var(--tm-end-y-991)));
  --tm-end-w:   calc(var(--tm-end-w-320) + var(--vpp-320-400) * (var(--tm-end-w-400) - var(--tm-end-w-320)) + var(--vpp-400-576) * (var(--tm-end-w-576) - var(--tm-end-w-400)) + var(--vpp-576-991) * (var(--tm-end-w-991) - var(--tm-end-w-576)) + var(--vpp-991-1440) * (var(--tm-end-w-1440) - var(--tm-end-w-991)));
  --tm-x: calc(var(--tm-start-x) + var(--scroll-progress) * (var(--tm-end-x) - var(--tm-start-x)));
  --tm-y: calc(var(--tm-start-y) + var(--scroll-progress) * (var(--tm-end-y) - var(--tm-start-y)));
  --tm-w: calc(var(--tm-start-w) + var(--scroll-progress) * (var(--tm-end-w) - var(--tm-start-w)));

  /* Bg vp-blended start + end */
  --bg-start-tx:    calc(var(--bg-start-tx-320) + var(--vpp-320-400) * (var(--bg-start-tx-400) - var(--bg-start-tx-320)) + var(--vpp-400-576) * (var(--bg-start-tx-576) - var(--bg-start-tx-400)) + var(--vpp-576-991) * (var(--bg-start-tx-991) - var(--bg-start-tx-576)) + var(--vpp-991-1440) * (var(--bg-start-tx-1440) - var(--bg-start-tx-991)));
  --bg-start-ty:    calc(var(--bg-start-ty-320) + var(--vpp-320-400) * (var(--bg-start-ty-400) - var(--bg-start-ty-320)) + var(--vpp-400-576) * (var(--bg-start-ty-576) - var(--bg-start-ty-400)) + var(--vpp-576-991) * (var(--bg-start-ty-991) - var(--bg-start-ty-576)) + var(--vpp-991-1440) * (var(--bg-start-ty-1440) - var(--bg-start-ty-991)));
  --bg-start-scale: calc(var(--bg-start-scale-320) + var(--vpp-320-400) * (var(--bg-start-scale-400) - var(--bg-start-scale-320)) + var(--vpp-400-576) * (var(--bg-start-scale-576) - var(--bg-start-scale-400)) + var(--vpp-576-991) * (var(--bg-start-scale-991) - var(--bg-start-scale-576)) + var(--vpp-991-1440) * (var(--bg-start-scale-1440) - var(--bg-start-scale-991)));
  --bg-end-tx:      calc(var(--bg-end-tx-320) + var(--vpp-320-400) * (var(--bg-end-tx-400) - var(--bg-end-tx-320)) + var(--vpp-400-576) * (var(--bg-end-tx-576) - var(--bg-end-tx-400)) + var(--vpp-576-991) * (var(--bg-end-tx-991) - var(--bg-end-tx-576)) + var(--vpp-991-1440) * (var(--bg-end-tx-1440) - var(--bg-end-tx-991)));
  --bg-end-ty:      calc(var(--bg-end-ty-320) + var(--vpp-320-400) * (var(--bg-end-ty-400) - var(--bg-end-ty-320)) + var(--vpp-400-576) * (var(--bg-end-ty-576) - var(--bg-end-ty-400)) + var(--vpp-576-991) * (var(--bg-end-ty-991) - var(--bg-end-ty-576)) + var(--vpp-991-1440) * (var(--bg-end-ty-1440) - var(--bg-end-ty-991)));
  --bg-end-scale:   calc(var(--bg-end-scale-320) + var(--vpp-320-400) * (var(--bg-end-scale-400) - var(--bg-end-scale-320)) + var(--vpp-400-576) * (var(--bg-end-scale-576) - var(--bg-end-scale-400)) + var(--vpp-576-991) * (var(--bg-end-scale-991) - var(--bg-end-scale-576)) + var(--vpp-991-1440) * (var(--bg-end-scale-1440) - var(--bg-end-scale-991)));
  --bg-tx:    calc(var(--bg-start-tx)    + var(--scroll-progress) * (var(--bg-end-tx)    - var(--bg-start-tx)));
  --bg-ty:    calc(var(--bg-start-ty)    + var(--scroll-progress) * (var(--bg-end-ty)    - var(--bg-start-ty)));
  --bg-scale: calc(var(--bg-start-scale) + var(--scroll-progress) * (var(--bg-end-scale) - var(--bg-start-scale)));

  /* Set by JS rAF each scroll event on :root (document.documentElement).
     MUST be set on :root not on a descendant; the intermediate blend
     vars above are computed at :root and would otherwise read the
     initial value (0) instead of the descendant's override.
     See feedback_custom_prop_var_resolution (23.04.26). */
  --scroll-progress: 0;

  /* ---- Entrance reveal REMOVED (23.04.26 v3) ----
     The local frosted-glass entrance layer was removed in v3. The UCB
     (Unified Chamber Backdrop) glass layer will handle the chamber-to-
     TEotE hand-off at the site level: glass eases in over the flooded
     chamber, covers the transition, then eases out revealing TEotE
     already pinned at its start composition. TEotE must not re-implement
     that fade locally.

     --entrance-progress is now a constant 1 so the chrome consumers
     (logo, breadcrumb, tabs, card, credit) that still reference it
     resolve to fully visible. The variable is retained rather than
     deleted because several rules still reference it; this avoids
     silent invalid-declaration fallbacks across the file. When UCB is
     in place this hook is available if we ever want to pair chrome
     opacity with the UCB glass easing-out curve. */
  --entrance-progress: 1;

  /* 19.05.26: chamber-into-TEoTE cross-fade hook. et-chamber.js writes
     this value 0 -> 1 across Phase E (in step with --chamber-out on
     .et-chamber). The TEoTE stage-wrap consumes it as opacity so the
     section eases in as the chamber dissolves out, replacing the
     prior hard cut. Default 1 so non-JS / pre-JS paint shows TEoTE
     at full opacity. */
  --teote-in: 1;

  /* 19.05.26 (later pass): staged second-wave reveal. The user wants
     Toby+Molly + the title logo to come in FIRST out of the chamber
     dissolve, with the rest of the content (background scenery, Malus
     vapour, atmospheric veil, callout glass, artist credit) following
     a beat later. --teote-late lags --teote-in by 30 percent of the
     dissolve, then ramps 0 -> 1 across the remaining 70 percent. It
     is applied multiplicatively on TOP of the existing per-element
     opacities, so the wrapper opacity = --teote-in still drives the
     chamber crossfade for ALL elements; --teote-late just holds the
     late wave at 0 until the dissolve is partway through. Default 1
     so non-JS / pre-JS paint shows everything at full opacity.

     Toby+Molly + logo group are NOT multiplied by --teote-late, so
     they ramp at the rate of the wrapper (= --teote-in) alone. The
     callout cards already have their own scroll-progress reveals
     (reveal-1 / reveal-2) that fire only as the user scrolls into
     the section, so they sit naturally AFTER the late wave too.

     19.05.26 (later x4): late wave brought forward per user direction
     ("delayed layers need to ease in a bit earlier"). Window is now
     scroll-progress 0.55 -> 0.95, i.e. starts revealing 55 percent
     through the TEoTE parallax and is fully resolved 5 percent before
     the section exits. Earlier versions held the late wave to 0.85
     onward which felt too late; bringing it forward to ~0.55 gives
     the scenery time to settle before the chamber-out transition. */
  --teote-late: clamp(0, calc((var(--scroll-progress, 1) - 0.55) * 2.5), 1);
}

/* ---------- Parallax track ----------
   300vh wrapper. The sticky stage stays pinned to the viewport for the full
   height of the track, so scroll travel is consumed entirely by layer parallax.

   18.05.26 (revised): TEoTE overlaps the end of VotF so it is sticky-pinned
   behind the dissolving chamber. Pull-up is (nav-height + 30px + 50vh):
     - nav-height + 30px positions the track top so the user lands on TEoTE
       starting frame 30px before VotF's bottom crosses the nav (matches
       et-chamber.js phaseE.end);
     - +50vh of pre-roll holds --scroll-progress at 0 during the dissolve
       (matches phaseE duration), so TEoTE is revealed at its true starting
       visual frame rather than mid-parallax.
   z-index: -1 sits TEoTE behind the chamber stage-wrap (which is in
   .et-chamber's isolated stacking context but renders above main-level
   negative z). After the chamber hits opacity 0, TEoTE shows through. */
/* 21.05.26 r24: TEOTE hidden initially (opacity 0), revealed via
   --teote-reveal flip when its track top reaches viewport top
   (= the parallax start position). After reveal, section-teote.js
   continues to drive --scroll-progress as normal so the parallax
   animation runs as the user scrolls into the track.
   r25 (19.05.26): gate moved from .teote-parallax-track to
   .teote-stage-wrap. Putting opacity on the track also faded its
   own #1a1617 background to 0, exposing the white body bg between
   the chamber's dark close and TEoTE's pin point. The dark band
   has to stay continuous from chamber -> chamber padding-bottom
   buffer -> track bg -> TEoTE. Gating the stage-wrap (which holds
   the actual visible bg image + content) keeps the track bg solid
   while still hiding TEoTE itself until pin time. */
.teote-parallax-track {
  position: relative;
  /* 22.05.26 (glass #3 / curtain): extra scroll length appended to the
     track AFTER the parallax completes. section-teote.js caps the parallax
     travel by this same distance so --scroll-progress reaches 1 (final
     frame LOCKED) a curtain-zone before the track end, then holds at 1
     while the sticky stage stays pinned through the extra height;
     section-observers.js rises the Contact section up over the locked
     frame across this zone. Unitless vh, consumed by the doubled-
     specificity height override below (which beats the plain 300vh in
     section-our-stories-intro.css) and read by both scripts. */
  --teote-curtain-vh: 100;
  /* 19.05.26: was -1 (sat behind chamber via negative-z below
     MAIN). Now z:0, paired with .et-chamber being pinned at z:1
     above this in section-et-chamber.css. Chamber still wins
     visually (chamber z:1 > parallax-track z:0), but TEoTE content
     can now win hit-tests against MAIN (which is z:auto). Without
     this, clicks on the TEoTE cover (i) were silently caught by
     MAIN because MAIN was the topmost hittable layer over the
     z:-1 parallax-track contents. */
  z-index: 0;
  /* 19.05.26 (later x3): negative scroll-margin-top calibrated so
     anchor jumps land at scroll-progress = 0 exactly - the very
     first frame of TEoTE's parallax where Toby+Molly is at its
     starting position about to begin moving, only the title logo /
     author / (i) / artist credit visible, and the scenery + Malus +
     callout cards + glass all still at opacity 0 (gated behind
     --teote-late and --reveal-1/2). The value equals -preRoll, where
     preRoll = nav-height - 95 + 25vh per section-teote.js
     updateProgress(). User direction: "TEoTE should load in to the
     very first frame of molly and toby image beginning to move". */
  scroll-margin-top: calc(95px - var(--nav-height) - 25vh);
  /* 19.05.26 (pm v3): offset bumped 70 -> 95 per user (+25 more
     delay on top of the previous +100, total +125 vs original). Must
     stay in sync with CHAMBER_DISSOLVE_END_OFFSET in et-chamber.js +
     section-teote.js. */
  margin-top: calc(-1 * (var(--nav-height) - 95px + 25vh));
  /* Track height includes the overlap pull-up so the visible 300vh of
     parallax travel is preserved AFTER the pre-roll consumes the
     overlap distance. */
  height: calc(300vh + var(--nav-height) - 95px + 25vh);
  /* 11.05.26 HEDGE: explicit min-height backstop in case the `height: 300vh`
     declaration is dropped or computes wrong on some browsers (Honor Chrome
     reported a collapsed TEoTE section that pointed at the parallax-track
     height failing -> sticky stage has no scroll dwell -> layers stack on top
     of each other in one viewport-height). Removable once root cause known. */
  min-height: calc(300vh + var(--nav-height) - 95px + 25vh);
  /* r30 (19.05.26): bg pure black to match the chamber's dipped state
     (filter:brightness(0) renders chamber as #000) AND the OS->TEoTE
     dark plate (now also #000 via .section--dark-transition--osteote).
     Track bg is only seen during the brief pre-TEoTE-reveal window
     where the stage-wrap is opacity 0; one uniform black band from
     chamber-dip through plate through track until TEoTE's bg image
     reveals. r29 used #0F1419 (matching un-dimmed chamber) but that
     also required killing the chamber filter, which left the white
     OS tile cards visible at the top of the viewport instead of
     dimming away with the chamber. Pure black + restored chamber
     filter handles both. */
  background: #000;
}

/* 22.05.26 (glass #3): AUTHORITATIVE track height. The base rule above is
   overridden by section-our-stories-intro.css, which forces the track to a
   plain 300vh via a doubled-class selector
   (.teote-parallax-track.teote-parallax-track). We match that specificity
   here and win on source order (this file loads after it) to APPEND the
   curtain zone: an extra --teote-curtain-vh of scroll AFTER the parallax
   completes, during which the sticky stage stays pinned (final frame
   LOCKED) while section-observers.js rises Contact up over it. */
.teote-parallax-track.teote-parallax-track {
  height: calc(300vh + (var(--teote-curtain-vh, 0) * 1vh));
  min-height: calc(300vh + (var(--teote-curtain-vh, 0) * 1vh));
}

/* ---------- Stage (sticky container) ----------
   container-type: size so chrome tokens (cqh/cqw) reference stage dimensions.
   isolation: isolate keeps all internal z-indexes out of the root stacking context,
   so nav at z:100 and any root overlays are unaffected. */
.teote-stage-wrap {
  position: sticky;
  top: 0;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  /* 11.05.26 HEDGE: min-height floor at 100vh in case the dvh override
     computes to a smaller value on some browsers (or returns 0 in some
     context). Without a guaranteed stage height, the absolute-positioned
     callout stack + credit + logo group all collapse to small Y values
     and overlap visually. */
  min-height: 100vh;
  overflow: hidden;
  background: #1a1617;
  container-type: size;
  container-name: teote-stage;
  isolation: isolate;
  /* 19.05.26: cross-fade in as the chamber dissolves out. --teote-in
     is written 0 -> 1 across Phase E by et-chamber.js (mirrors
     --chamber-out). Before Phase E the chamber sits on top at full
     opacity so the stage-wrap being at 0 is hidden anyway; after
     Phase E --teote-in stays at 1 for the rest of the section.
     r25 (19.05.26): also consumes --teote-reveal (set on the parent
     .teote-parallax-track by section-observers.js). Default 0 so the
     stage-wrap is invisible until track.top <= 0; the track itself
     keeps its #1a1617 bg solid, so the dark band stays continuous
     during the chamber -> TEoTE handoff. Multiplied with --teote-in
     so the legacy dissolve var is preserved as a no-op (its default
     1 leaves the reveal gate authoritative). */
  opacity: calc(var(--teote-in, 1) * var(--teote-reveal, 0));
  transition: opacity 500ms ease-out;
}

/* Frosted-glass entrance layer REMOVED in v3 (23.04.26).
   UCB glass will handle the chamber-to-TEotE transition at the site
   level. No local entrance fade on this section. */

/* ---------- Art canvas (cover-fit) ----------
   6951:4805 native aspect of the bg-wide asset. max() rather than min() so
   the canvas covers the full stage on both axes (cover behaviour). Overlays
   inside are positioned as % of the canvas so the 1440 composition stays
   proportionally consistent at every viewport width. */
.teote-canvas {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width:  max(100cqw, calc(100cqh * 6951 / 4805));
  height: max(100cqh, calc(100cqw * 4805 / 6951));
  container-type: size;
  container-name: teote-canvas;
  z-index: 0;
}

/* ---------- Background layer ---------- */
.teote-stage__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.teote-stage__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--bg-x) var(--bg-y);
  /* Translate + scale are pre-blended in :root from the four-anchor
     start/end tokens and --scroll-progress. The transform here is a
     direct read. transform-origin: center top preserves the top-edge
     pinned-below-nav behaviour per feedback_background_top_below_nav. */
  transform: translate(var(--bg-tx), var(--bg-ty)) scale(var(--bg-scale));
  transform-origin: center top;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
  /* 19.05.26: --teote-late delays the scenery's appearance so the
     toby+molly + logo wave reads first against the dark TEoTE backdrop
     as the chamber dissolves.
     20.05.26: opacity now floors at 0.25 per Caoimhin - the bg should
     read at 25% from the moment TEoTE pins, then ramp to 100% as
     --teote-late goes 0 -> 1. Linear lerp: 0.25 at teote-late=0, 1.0
     at teote-late=1. */
  opacity: calc(0.25 + 0.75 * var(--teote-late, 1));
}

/* ---------- Overlay layers ----------
   Positioned as % of the canvas. The resolved --ov-* tokens are set per
   overlay via the --malus-* and --tm-* custom properties above. */
.teote-overlay {
  position: absolute;
  left: var(--ov-x, 50%);
  top:  var(--ov-y, 50%);
  width: var(--ov-w, 50%);
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 0;
}
.teote-overlay img {
  display: block;
  width: 100%;
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
}

/* Malus: vapour atmospheric layer.
   Saturate rises with scroll (1 -> 1.15) so the cloud reads more
   vivid as the parallax progresses; pairs with the scroll-driven
   --malus-opacity calc set in :root. */
.teote-overlay--malus {
  --ov-x: var(--malus-x);
  --ov-y: var(--malus-y);
  --ov-w: var(--malus-w);
  mix-blend-mode: var(--malus-blend);
  /* 19.05.26 (later): Malus vapour now eases in starting at the
     HALFWAY point between TEoTE loading in and reaching its final
     frame (scroll-progress 0.5 -> 1.0), independent of the broader
     --teote-late wave. Pure linear ramp via clamp(); the smooth
     "ease-in" feel comes from the scroll being a continuous user
     input over a long range. Earlier than the other late layers
     (bg, veil, glass, credit) per user direction. */
  opacity: calc(var(--malus-opacity) * clamp(0, calc((var(--scroll-progress, 1) - 0.5) * 2), 1));
  filter:
    blur(var(--malus-blur))
    saturate(calc(1 + var(--scroll-progress, 0) * 0.15));
  transition: opacity 180ms linear;
}
.teote-overlay--malus .malus-drift {
  animation: malusDrift 14s ease-in-out infinite;
  /* 11.05.26: will-change moved to the .teote-is-near gated rule below.
     Previously held a GPU layer for the lifetime of the page even when
     TEoTE was scrolled past. */
}
/* Slow upward stretch on the cloud image (origin bottom centre so
   the floor stays anchored). Pairs with the rising opacity + saturate. */
.teote-overlay--malus img {
  transform: scaleY(calc(1 + var(--scroll-progress, 0) * var(--malus-stretch-max, 0.15)));
  transform-origin: 50% 100%;
  transition: transform 240ms linear;
}
@keyframes malusDrift {
  0%, 100% { transform: translate(0, 0); }
  25%      { transform: translate(var(--malus-drift-x),            calc(var(--malus-drift-y) * -0.3)); }
  50%      { transform: translate(0,                               calc(var(--malus-drift-y) * -1)); }
  75%      { transform: translate(calc(var(--malus-drift-x) * -1), calc(var(--malus-drift-y) * -0.3)); }
}

/* Toby + Molly: foreground figures. */
.teote-overlay--tm {
  --ov-x: var(--tm-x);
  --ov-y: var(--tm-y);
  --ov-w: var(--tm-w);
  opacity: var(--tm-opacity);
}
.teote-overlay--tm img {
  /* Scroll parallax is absorbed into --tm-x/y/w via the :root start→end
     blend, so the img stays identity here. */
}

/* Pinpoint eye system (.malus-eye-anchor + .malus-eye + .toby-eye-glow)
   REMOVED 12.05.26. The Malus DOM block was already commented-out on
   11.05.26; the dormant CSS rules + per-BP vars + JS rAF tracker
   (~200 lines of JS + ~40 lines of CSS) cleaned up today. The Toby
   eye-glow layer was the second-heaviest per-frame paint cost on
   TEoTE (drop-shadow + blur(1.5px) + mix-blend-mode: overlay chain).
   Full restoration recipes preserved in
   archive/fix-v3-120526-pre-toby-eye-removal.section-teote.css and
   archive/fix-v3-120526-pre-malus-eye-cleanup.section-teote.css. */

/* ---------- will-change gate ----------
   Applied only while the track is near/in viewport (JS toggles .teote-is-near).
   Only the BG img uses transform in the new model, so only that layer needs
   GPU promotion. Overlays are animated via left/top/width changes absorbed
   in the :root blend; no transform-based compositing layer to gate. */
.teote-is-near .teote-stage__bg img {
  will-change: transform;
}
/* 11.05.26: malus-drift GPU promotion gated alongside the bg img so the
   drift's compositor layer is only held while TEoTE is near the viewport. */
.teote-is-near .teote-overlay--malus .malus-drift {
  will-change: transform;
}

/* ---------- Veil ----------
   Soft top/bottom darkening so chrome elements always have contrast. */
.teote-stage__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(180deg,
      rgba(0, 0, 0, 0.35) 0%,
      rgba(0, 0, 0, 0.0) 25%,
      rgba(0, 0, 0, 0.0) 60%,
      rgba(0, 0, 0, 0.5) 100%),
    radial-gradient(ellipse at 25% 55%,
      rgba(0, 0, 0, 0.0) 0%,
      rgba(0, 0, 0, 0.0) 45%,
      rgba(0, 0, 0, 0.35) 100%);
  pointer-events: none;
  /* 19.05.26: atmospheric veil joins the late wave; the dark backdrop
     of the stage already serves as the early "from the black" surface. */
  opacity: var(--teote-late, 1);
}

/* ---------- Title lockup ----------
   .teote-logo-group: absolutely positioned container for the text-only
   logo asset (aspect 2.10, unified across all breakpoints) and the
   "by L.E Belle" author credit beneath it. */
.teote-logo-group {
  position: absolute;
  /* 19.05.26 morning bump 2 -> 4 reverted: the (i) trigger now lives
     OUTSIDE this group (as a sibling) with its own z-index, so the
     group no longer needs to stack above the callout glass. */
  z-index: 2;
  left: var(--logo-x);
  top: var(--logo-y);
  transform: translateX(-50%);
  width: var(--logo-size);
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
  /* 21.05.26 r16: unified three-stack drop-shadow shared with the ET
     intro lockup + hero .logo-pin__logo. Tight contact, mid lift,
     soft ambient. */
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.65))
          drop-shadow(0 3px 8px rgba(0, 0, 0, 0.5))
          drop-shadow(0 6px 24px rgba(0, 0, 0, 0.36));
  /* The logo rises in as part of the whole-stage entrance (see
     .teote-stage-wrap, r37) - no separate logo animation.
     r59 (21.05.26): multiplied by --teote-ui-in so the title text waits
     until the "Some worlds we ought to know" glass #2 title has cleared
     (driven 0->1 by section-observers.js). Default 1 = no change pre-JS. */
  opacity: calc(var(--entrance-progress) * var(--teote-ui-in, 1));
}
.teote-logo {
  width: 100%;
  height: auto;
  display: block;
}
.teote-logo-author {
  display: block;
  margin-top: 10px;
  font-family: var(--font-serif);
  font-style: italic;
  /* 18px above 576, 15px below (see media query). The earlier per-
     breakpoint shrinks at 480 / 400 / 320 were dropped at graduation. */
  font-size: 18px;
  color: var(--soft-text);
  letter-spacing: 0.02em;
  text-shadow:
    0 1px 2px rgba(0, 0, 0, 0.85),
    0 0 6px rgba(0, 0, 0, 0.55);
}

/* 19.05.26 (later): (i) cover trigger sits as a SIBLING of
   .teote-logo-group, anchored to the BOTTOM of the stage just above
   the artist credit. (Was previously anchored just below the logo
   group at top:calc(--logo-y + --logo-group-height + 12px).) Moved
   to bottom-anchor per user direction: the (i) sits above the credit,
   and the credit must always be readable - bottom anchoring keeps
   their vertical relationship stable across viewport heights.
     - bottom: stacks above the credit line (which sits at bottom:50
       desktop, bottom:18 at max-height:800). The +28+8 leaves room
       for the credit height + a small gap.
     - z-index: 5 sits above the callout-stack (z:3) so the icon is
       visually unobscured by the callout glass.
   The lightbox JS injects the standard .lightbox-trigger button
   inside this figure; img is hidden and purely a pointer for the
   lightbox to read the srcset off. */
.teote-cover-trigger {
  position: absolute;
  top: auto;
  bottom: calc(50px + 28px + 8px);   /* credit bottom + credit-height-ish + gap */
  /* 22.05.26: right-corner anchored to sit directly above the artist
     credit (also moved bottom-right). Right gutter matches .teote-credit. */
  right: clamp(20px, 4vw, 50px);
  left: auto;
  transform: none;
  z-index: 5;
  display: flex;
  justify-content: flex-end;
  margin: 0;
  padding: 0;
  pointer-events: auto;
}
.teote-cover-trigger img {
  display: none;
}
.teote-cover-trigger .lightbox-trigger {
  /* Override the default bottom-right absolute pinning. With the
     figure now position: absolute (parent), the button sits in
     normal flow inside it. position: relative gives the button its
     own stacking context for z-index support. pointer-events: auto
     is asserted explicitly for clarity. */
  position: relative;
  right: auto;
  bottom: auto;
  pointer-events: auto;
}

/* ==========================================================
   Central callout layer (graduated 10.05.26 from sandbox)
   Glass backdrop wrapping the descriptive headline + auto-cycling
   secondary block (Pops's letter / Malus quote, 10s interval). The
   stack is block-flow auto-height; the glass is absolute with
   top:-83 / bottom:-83 so it stretches 83px above and below the
   contained text. Replaces the prior tabs + crystal-card panel
   system entirely.
   ========================================================== */

.teote-callout {
  pointer-events: none;
  font-family: var(--font-serif);
  /* 18.05.26: callout main text flipped to primary-text (white on
     dark theme) per user. Quote sigs (.teote-callout cite below)
     keep their quiet-text colour. */
  color: var(--primary-text);
  text-align: center;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.85);
}
.teote-callout p { margin: 0; }
.teote-callout cite {
  display: block;
  margin-top: 10px;
  font-size: 0.78em;
  font-style: italic;
  color: var(--quiet-text);
  letter-spacing: 0.02em;
}
/* 19.05.26 (pm): the .teote-callout reset matches headline descendants
   only; .teote-callout-secondary's blockquote carries the browser
   default ~40px L/R margin, leaving the bottom (secondary) text
   ~80px narrower than the top (headline). Same reset applied
   explicitly to the secondary so both text parts share the same
   content width. */
.teote-callout blockquote,
.teote-callout-secondary blockquote { margin: 0; padding: 0; }

.teote-callout-stack {
  position: absolute;
  z-index: 3;
  left: 50%;
  /* 11.05.26 HEDGE: explicit numeric fallback before the calc so that if
     any var in the calc resolves to invalid (e.g. --logo-group-height
     evaluated in a context where its inner calc fails), the declaration
     is discarded by the cascade and this fallback takes effect rather
     than `top` becoming `auto` (which would stack the callout at the
     top of its containing block alongside other absolute children). */
  top: 30vh;
  top: calc(var(--logo-y) + var(--logo-group-height) + var(--callout-gap));
  transform: translateX(-50%);
  width: var(--callout-stack-width);
  display: block;
}

/* Glass plate. Width 720, top:-113 / bottom:-113 (symmetric so text
   stays pinned to centre). Bumped 30px each side from the locked
   sandbox 83/83 on 10.05.26 graduation. Clips past viewport L/R at
   narrow widths (intentional). SVG inside uses preserveAspectRatio
   "none" so the irregular crystal stretches to fit. */
.teote-callout-glass {
  position: absolute;
  /* 18.05.26 (pm v3): glass frame tightened (top/bottom pulled in
     from -113 to -90) per user direction. Text inside is now
     smaller (clamp 12-17 vs prior 14-22) so the previous generous
     113px extension read as floating. */
  top: -90px;
  bottom: -90px;
  left: 50%;
  z-index: 0;
  /* 19.05.26 (pm v2): width bumped +10% to 842px alongside the stack
     bump from 735 to 808. Overhang stays ~17px each side. */
  width: 842px;
  height: auto;
  transform: translate(-50%, 0) translateZ(0);
  pointer-events: none;
  /* 19.05.26: glass joins the late wave so it doesn't sit empty over
     the toby+molly + logo first wave. The callout cards themselves
     already reveal later via scroll-progress (reveal-1 / reveal-2). */
  opacity: calc(var(--entrance-progress) * 0.62 * var(--teote-late, 1));
  filter: drop-shadow(0 12px 32px rgba(0, 0, 0, 0.45));
  overflow: visible;
}
/* GPU layer: rasterise the displacement filter once and composite
   only opacity per frame. Gated behind .teote-is-near so the layer
   isn't held when the section is far off-screen. */
.teote-is-near .teote-callout-glass {
  will-change: opacity;
}
/* 22.05.26 (step 3 follow-up): promote the callout cards while the section
   is near the viewport so their scroll-driven reveal transform (translateY
   + scale via --reveal-1/2) is composited instead of re-painting each
   card's box-shadow + text-shadow on every scroll frame. That per-frame
   repaint was the residual stutter on the text cards animating in (most
   visible on touch, where the rest of the scene is now frozen static).
   No visual change; gated by .teote-is-near like the bg + glass so the
   GPU layers are not held while the section is off-screen. */
.teote-is-near .teote-callout-card {
  will-change: transform;
}
/* 11.05.26: glass swapped from inline SVG (feTurbulence + feDisplacementMap)
   to a pre-rasterised PNG/AVIF in a <picture>. The image is stretched
   non-uniformly via object-fit: fill (the default for <img>) to match the
   former preserveAspectRatio="none" behaviour. */
.teote-callout-glass picture {
  display: block;
  width: 100%;
  height: 100%;
}
.teote-callout-glass img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: fill;
}

/* Headline. Padding-y is 0 so text-top sits at stack-top (the glass
   top:-83 anchor lands 83px above text-top). position: relative +
   z-index: 1 so text paints above the glass; z-index needs a
   positioned element. */
/* Per-paragraph cards (18.05.26 pm v4): each headline paragraph now
   sits inside its own card per user. The single ornate crystal-glass
   PNG behind both paragraphs is retired. Each card is a semi-
   transparent dark fill with asymmetric border-radius so it reads
   as a "random rectangle" - roughly geometric but softened with
   irregular corner radii. Two variants give visual variety between
   the cards (different corner profiles) so they don't read as
   stamped duplicates. */
.teote-callout-card {
  position: relative;
  z-index: 1;
  padding: clamp(16px, 2vw, 28px) clamp(22px, 4vw, 46px);
  /* 19.05.26 (later): bg gradient alpha increased per user (0.45 -> 0.78,
     0.55 -> 0.85) so the card background reads more solid behind the
     text. The cards now sit on the scenery + Malus vapour as those late
     layers fade in, and the earlier 0.45/0.55 alphas left the text fighting
     for legibility against the busier background. */
  background: linear-gradient(135deg, rgba(20, 15, 25, 0.78), rgba(15, 12, 20, 0.85));
  border: 1px solid rgba(255, 255, 255, 0.07);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  font-family: var(--font-serif);
  font-style: normal;
  font-weight: 400;
  /* 18.05.26 (pm v3): smaller fluid clamp per user. 12px at 320
     viewport scaling to 17px at 1440+ (was 14-22px). */
  font-size: clamp(12px, 0.45vw + 10.5px, 17px);
  line-height: 1.6;
  color: var(--primary-text);
  text-align: center;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.85);
}
.teote-callout-card + .teote-callout-card {
  margin-top: clamp(14px, 1.8vw, 22px);
}
.teote-callout-card p { margin: 0; }
.teote-callout-card em {
  font-style: italic;
  color: var(--primary-text);
}

/* Two corner-radius variants so the two cards read as siblings, not
   stamped duplicates. Each border-radius value is split into x/y
   axes so corners get an organic, slightly-uneven curvature.
   Per-card scroll reveal (18.05.26 pm v5): more dynamic motion per
   user. Now combines a larger upward slide (30px -> 0) with a
   slight zoom-in (scale 0.94 -> 1) on top of the opacity ramp. The
   card AND its text fade in together so the viewer never sees an
   empty frame.
     - Card 1 (combined headline): scroll-progress 0.05 -> 0.25
     - Card 2 (rotating quotes):   scroll-progress 0.35 -> 0.55
   Card 2 is narrowed (max-width 76 percent) so the rotating quotes
   don't dominate the stage; the long Pops letter still reads, just
   in a more contained block. */
.teote-callout-card--1 {
  border-radius: 6px 14px 8px 12px / 12px 6px 14px 8px;
  --reveal-1: clamp(0, calc((var(--scroll-progress, 0) - 0.05) * 5), 1);
  opacity: calc(var(--entrance-progress, 1) * var(--reveal-1));
  transform: translateY(calc((1 - var(--reveal-1)) * 30px)) scale(calc(0.94 + var(--reveal-1) * 0.06));
}
.teote-callout-card--2 {
  border-radius: 10px 6px 14px 8px / 8px 12px 6px 14px;
  --reveal-2: clamp(0, calc((var(--scroll-progress, 0) - 0.35) * 5), 1);
  opacity: calc(var(--entrance-progress, 1) * var(--reveal-2));
  transform: translateY(calc((1 - var(--reveal-2)) * 30px)) scale(calc(0.94 + var(--reveal-2) * 0.06));
  max-width: 76%;
  margin-left: auto;
  margin-right: auto;
  padding: clamp(12px, 1.6vw, 22px) clamp(18px, 3vw, 36px);
}

/* Paragraph-level rules retired in pm v4: each paragraph is now
   inside its own .teote-callout-card; the card carries the reveal
   transform + opacity. */

/* Secondary callout. In-flow child of the stack, 25px below the
   headline. Holds Pops's letter and Malus's scorched-world quote;
   crossfades between them on a 10s interval driven by JS toggling
   data-active. Both items overlay in a single grid cell so the
   block height matches the taller item. */
.teote-callout-secondary {
  position: relative;
  z-index: 3;
  /* 18.05.26 (pm v5): now lives INSIDE .teote-callout-card--2 per
     user, so the old margin-top: 25px (separation from headline)
     and padding-x: var(--callout-text-pad-x) (column constraint)
     are dropped - the card handles both. Font size + line-height
     tightened so the rotating quotes occupy less vertical space. */
  margin-top: 0;
  padding: 0;
  width: 100%;
  pointer-events: none;
  opacity: var(--entrance-progress);
  font-family: var(--font-serif);
  color: var(--primary-text);
  text-align: center;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.85);
  font-size: clamp(10px, 0.3vw + 9px, 12.5px);
  line-height: 1.55;
  box-sizing: border-box;
  display: grid;
  grid-template-columns: 1fr;
}
.teote-callout-secondary__item {
  grid-column: 1;
  grid-row: 1;
  transition: opacity 600ms ease, visibility 0s linear 600ms;
  opacity: 0;
  visibility: hidden;
}
.teote-callout-secondary__item p { margin: 0; }
.teote-callout-secondary__item em { font-style: italic; color: var(--primary-text); }
.teote-callout-secondary__item cite {
  display: block;
  margin-top: 6px;
  font-size: 0.85em;
  font-style: italic;
  color: var(--quiet-text);
  letter-spacing: 0.02em;
}
.teote-callout-secondary[data-active="powerful"] .teote-callout-secondary__item--powerful,
.teote-callout-secondary[data-active="malus"]    .teote-callout-secondary__item--malus {
  opacity: 1;
  visibility: visible;
  transition: opacity 600ms ease, visibility 0s linear 0s;
}

/* Sub-576 overrides: text column scales with viewport (no longer
   locked at 700), padding-x ramps 30 -> 10 across 320-575.98, and
   the headline font-size scales down from 18 to 15. The glass
   stays 720 wide and clips past the viewport L/R, intentional. */
@media (max-width: 575.98px) {
  :root {
    --logo-size:           260px;
    --callout-stack-width: min(808px, calc(100vw - 32px));  /* r34: match the base (16px gutter) so cards never sit flush to / past the edges */
  }
  .teote-logo-author { font-size: 15px; }
  .teote-callout--headline {
    font-size: clamp(15px, 0.586vw + 11.6px, 18px);
  }
}
/* Sub-576 padding ramp retired (18.05.26 pm v5): both
   .teote-callout--headline (gone) and .teote-callout-secondary
   (now inside .teote-callout-card--2) are obsolete targets. The
   card's own clamp() padding handles narrow viewports without
   needing a separate ramp. */

/* ---------- Scroll-cue chevron (19.05.26 later) ----------
   Visible at scroll-progress = 0 (the first frame of TEoTE, where
   nav clicks land); fades to 0 across scroll-progress 0 -> 0.04 so
   the moment the user starts scrolling the cue retires. Moved to
   sit BELOW the author line ("by L.E. Belle") with a clean gap,
   rather than at the bottom of the stage (the bottom position
   overlapped the (i) cover trigger which is bottom-anchored above
   the artist credit). Bob animation mirrors the OS chamber scroll
   cue pattern so the visual idiom carries between sections. */
.teote-scroll-cue {
  position: absolute;
  z-index: 4;
  left: 50%;
  top: calc(var(--logo-y) + var(--logo-group-height) + 36px);
  transform: translateX(-50%);
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  /* r59 (21.05.26): chevron now comes in AFTER the "Some worlds we ought
     to know" title clears (per Caoimhin). Gated by --teote-ui-in (driven
     0->1 by section-observers.js once the title is gone). Its scroll-cue
     fade-out is re-timed to start LATER (sp 0.1 -> 0.2) - the old
     1 - sp*25 faded it out by sp 0.04, i.e. BEFORE the title cleared, so
     it was effectively never seen. Now: held full until sp 0.1, gone by
     sp 0.2, so there's a visible window after the title. */
  /* r60: scroll fade HELD FULL until sp ~0.12 then eased to 0 by ~0.28,
     so the chevron is at FULL opacity when the nav jump lands (~sp 0.10,
     trackTop+1.6*nav) and only fades as the user scrolls deeper. (The
     earlier 1 - sp*4 was already fading by the landing, so the cue came
     in dim.) The --teote-ui-in GATE (chevron waits for the glass #2
     title to clear) is on .teote-scroll-cue-chevron; net opacity = this
     fade x the gate (opacity compounds parent x child). */
  opacity: clamp(0, calc((0.28 - var(--scroll-progress, 0)) / 0.16), 1);
  transition: opacity 200ms ease;
}
.teote-scroll-cue-chevron {
  display: block;
  width: 14px;
  height: 14px;
  border-right: 2px solid rgba(255, 255, 255, 0.85);
  border-bottom: 2px solid rgba(255, 255, 255, 0.85);
  transform: rotate(45deg);
  animation: teoteScrollCueBob 1.8s ease-in-out infinite;
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.7));
  /* r59: the --teote-ui-in GATE (chevron waits for the glass #2 title to
     clear). Kept on this inner element as a simple var so it compounds
     with the container's scroll-fade opacity (parent x child) without
     tripping the renderer's calc(var * clamp) bug. */
  opacity: var(--teote-ui-in, 1);
}
@keyframes teoteScrollCueBob {
  0%, 100% { transform: rotate(45deg) translate(0, 0); }
  50%      { transform: rotate(45deg) translate(3px, 3px); }
}
@media (prefers-reduced-motion: reduce) {
  .teote-scroll-cue-chevron { animation: none; }
}

/* ---------- Credit line ----------
   Anchored to the bottom-centre of the sticky stage, 25px clear of
   the bottom edge. Italic text with drop-shadow for contrast on the
   dark background. Fades in with the rest of the chrome via
   --entrance-progress. Font-size matches .et-card__credit so the
   art credits read identically across all sections. */
.teote-credit {
  position: absolute;
  z-index: 3;
  /* 22.05.26: moved from bottom-centre to the bottom-right corner per
     user direction; pairs with the (i) trigger corner-anchored just above
     it. Right gutter scales 20px (phone) to 50px (desktop), roughly
     mirroring the bottom inset. */
  right: clamp(20px, 4vw, 50px);
  left: auto;
  bottom: 50px;   /* 15.05.26: shifted up 25px per Caoimhín (was 25px). */
  transform: none;
  margin: 0;
  padding: 0;
  font-family: var(--font-serif);
  font-style: italic;
  font-size: clamp(11px, 0.3vw + 10px, 12px);
  line-height: 1.5;
  /* 18.05.26: bottom credit flipped to primary-text (white) per
     user. Quote sigs (the cite elements inside the callouts above)
     stay at quiet-text. */
  color: var(--primary-text);
  text-align: right;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
  white-space: nowrap;
  /* 19.05.26 (later): credit reverted to always-visible (was tied to
     --teote-late). User direction: the artist credit must always be
     readable while the section is in view, and the (i) cover trigger
     sits just above it. Both are now anchored to the bottom of the
     stage and fade in with the wrapper opacity (--teote-in via the
     stage-wrap), not gated behind the late wave.
     r59 (21.05.26): multiplied by --teote-ui-in so the credit waits for
     the "Some worlds we ought to know" title to clear (per Caoimhin). */
  opacity: calc(var(--entrance-progress) * var(--teote-ui-in, 1));
}
.teote-credit a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid rgba(190, 183, 170, 0.25);
  transition: color 150ms ease, border-color 150ms ease;
}
.teote-credit a:hover {
  color: var(--accent-tint);
  border-bottom-color: var(--accent);
}

/* At narrow widths the callout-stack extends down far enough to
   collide with the bottom-anchored credit (the "Original Artwork by
   Grimsloki" overlap visible on phones). Re-anchor the credit to just
   below the logo group so it sits in the small gap above the stack,
   and the stack has the lower portion of the stage free to extend
   into. (15.05.26 fix.) */
/* 19.05.26 (later): collapse at 800px height happens across ALL widths,
   not just narrow phones. The previous gentle compression (--callout-gap
   60 -> 40) wasn't enough on its own - at 800px height the artist
   credit gets covered by the callout-stack / late-wave background as
   they fade in. Apply the layout-chrome tightening universally at
   max-height: 800px so all widths share the same compressed budget;
   keep the additional card-text tightening scoped to narrow widths
   below. */
@media (max-height: 800px) {
  :root {
    --logo-y: 110px;              /* was 175px */
    --callout-gap: 24px;          /* was 60px */
  }
  .teote-credit {
    bottom: 18px;                 /* was 50px */
  }
  .teote-cover-trigger {
    /* Track the credit's tighter bottom anchor so the (i) stays
       just above it. */
    bottom: calc(18px + 28px + 6px);
  }
  /* .teote-scroll-cue is now top-anchored to the logo-group; the
     bottom-override is no longer needed at narrow heights. */
}

/* At narrow + short (e.g. 320x800 portrait phone), additionally
   tighten the callout cards themselves so the longer wrapped text
   still fits. Wider viewports at 800px height don't need the
   per-card shrink because the cards fit comfortably side-of-padding
   at desktop card widths. */
@media (max-width: 575.98px) and (max-height: 800px) {
  .teote-callout-card {
    padding: clamp(12px, 2vw, 24px) clamp(18px, 4vw, 38px);
    /* Tighter line-height + slightly smaller font so each card
       reads in fewer lines at 320 wide. Card 2's max-width is
       already 76% so it stays narrower than card 1. */
    font-size: clamp(11px, 0.45vw + 9.5px, 14px);
    line-height: 1.5;
  }
  .teote-callout-card + .teote-callout-card {
    margin-top: clamp(8px, 1.4vw, 16px);   /* was 14-22 */
  }
}

/* ==========================================================
   Narrow-viewport chrome tightening
   Root fluid covers 320/576/1440. Media queries below tune
   480, 400, and 320 (which otherwise inherit the 576 anchor).
   ========================================================== */

/* The earlier per-breakpoint shrinks at 480 / 400 / 320 (logo-size,
   logo-author, tabs, crystal-card, panels) have all been dropped at
   the 10.05.26 graduation: tabs / panels / card no longer exist, and
   the logo + author + credit reverts now live in the single sub-576
   block in the callout-layer rules above. */

/* ==========================================================
   Reduced motion
   In the start→end blend model, pinning --scroll-progress to 0 freezes
   all layer parallax at the opening composition. Idle drift + shimmer
   + tab transitions are also cut explicitly.
   JS also pins --scroll-progress: 0 on :root (see section-teote.js) so
   the stage doesn't try to update it on scroll; this rule is belt-and-
   braces for user-agents that don't honour the JS matchMedia check.
   ========================================================== */
@media (prefers-reduced-motion: reduce) {
  :root {
    --scroll-progress: 0;
    /* Force entrance-progress to 1 so reduced-motion users see the full
       composition immediately. Without this override the entrance fade
       would stay at 0 indefinitely (scroll-progress pinned to 0) and
       the section would be invisible. */
    --entrance-progress: 1;
  }
  /* 18.05.26 (pm v4): per-card reveals also read --scroll-progress;
     with it pinned to 0 the cards would stay invisible. Pin both
     reveals to 1 and drop the slide transforms so reduced-motion
     users see the full callout composed. */
  .teote-callout-card--1,
  .teote-callout-card--2 {
    opacity: 1;
    transform: none;
  }
  .teote-callout-card--1 { --reveal-1: 1; }
  .teote-callout-card--2 { --reveal-2: 1; }
  .teote-overlay--malus .malus-drift { animation: none; }
}

/* ==========================================================
   Mobile perf budget (10.05.26)
   On phones the per-frame scroll-driven filter recomputation
   on the Malus cloud + the SVG turbulence filter rasterisation
   were the main causes of TEoTE stutter (iOS + Honor reports).
   Below 768 the section drops these effects and renders the
   composition statically. Visual: cloud still floats and parallaxes
   via --malus-x/y, headline + secondary still cycle, eye tracker
   still runs (it's lightweight after the IO gate). What's lost:
   the cloud's slow upward stretch, the saturation lift on scroll,
   the irregular crystal turbulence, the 14s idle drift animation.
   ========================================================== */
@media (max-width: 768px) {
  /* 11.05.26: the SVG glass turbulence/displacement strip previously
     here was REMOVED per Caoimhín. The original purple crystal glass
     now renders identically at iPad+ breakpoints. (Phone-only re-strip
     is in the ≤576 block below; iPad keeps the full glass.) The
     Malus strips below stay at ≤768 since phones + small tablets
     all benefit from dropping per-frame saturate / scaleY / drift. */
  /* Malus cloud: drop the per-frame saturate() (was the heaviest
     per-pixel scroll cost). Static blur retained. */
  .teote-overlay--malus {
    filter: blur(var(--malus-blur));
    transition: none;
  }
  /* Malus cloud img: drop the scaleY scroll stretch so the GPU
     layer doesn't re-rasterise every frame. */
  .teote-overlay--malus img {
    transform: none;
    transition: none;
  }
  /* Drop the idle drift animation; one less compositing layer
     working continuously. */
  .teote-overlay--malus .malus-drift {
    animation: none;
  }
}

/* ==========================================================
   Touch static-scene mode (22.05.26, session 3, step 3)
   Frame rate on phones/tablets dropped under the live parallax: the
   figure layers animate left / top / width (per-frame LAYOUT), the bg
   animates scale (per-frame re-raster), and the ~83-entry calc cascade
   recomputes every scroll frame. Per user direction, on touch we PIN the
   whole scene at its end frame (no scroll movement) and let only the text
   callouts reveal on scroll, with the particle canvas (see atmosphere.js)
   and the Malus vapour switched back on. Desktop (fine pointer) keeps the
   full moving parallax untouched.

   Mechanism: shadow the computed position vars with their END tokens.
   Those tokens are scroll-independent and resolve at :root per viewport,
   so overriding the consuming vars on the section subtree is safe (it does
   NOT rely on a descendant --scroll-progress, which the :root blend would
   ignore). The scenery opacity wave (--teote-late) is pinned to 1 on
   :root; the text callouts ramp on --reveal-1/2, NOT --teote-late, so they
   still animate in. This block also REPLACES the earlier blend strip:
   with the mix-blend-mode:normal override gone, the vapour uses its
   lighten blend again. */
@media (pointer: coarse) {
  :root {
    /* scenery opacity wave to full (bg / veil / glass). Does not touch the
       text callouts, which ramp on --reveal-1/2. */
    --teote-late: 1;
  }
  #the-ends-of-the-earth {
    --bg-tx:    var(--bg-end-tx);
    --bg-ty:    var(--bg-end-ty);
    --bg-scale: var(--bg-end-scale);
    --tm-x: var(--tm-end-x);
    --tm-y: var(--tm-end-y);
    --tm-w: var(--tm-end-w);
    --malus-x: var(--malus-end-x);
    --malus-y: var(--malus-end-y);
    --malus-w: var(--malus-end-w);
    /* pin the Malus opacity ramp to its end so the vapour reads at full
       strength from the moment the section pins. */
    --malus-opacity: calc(0.40 * var(--malus-vortex-flicker, 1));
  }
  /* vapour visible throughout: drop the scroll-driven 0.5 -> 1 fade clamp. */
  .teote-overlay--malus {
    opacity: var(--malus-opacity);
  }
}

/* Phone-only FPS strip block REMOVED 12.05.26. The block
   carried per-frame mitigations for two heavy layers:
   (1) the SVG glass turbulence chain (rasterised to static
       AVIF/PNG on 11.05.26, no longer a per-frame cost), and
   (2) the Toby ellipse blur (the entire .toby-eye-glow layer
       removed on 12.05.26). Both mitigations are now obsolete. */

/* ==========================================================
   Container-query fallback
   Very old browsers without container-query support (pre-2022 Safari/
   Chrome/Firefox). Substitutes vh/vw for cqh/cqw on the critical
   positioning tokens so the section does not collapse. Bg translate
   start/end tokens are set to viewport-unit equivalents using the
   1440 anchor values (acceptable graceful degradation; mobile
   crossfade precision is lost at this point but the composition
   still reads correctly on desktop). Usage share of affected browsers
   is sub-1% in 2026.
   ========================================================== */
@supports not (container-type: size) {
  :root {
    --breadcrumb-y: calc(var(--nav-height, 100px) + 12px);
    /* Collapse bg motion to the 1440 anchor in viewport units. */
    --bg-start-tx:    -0.5vw;
    --bg-start-ty:    -14vh;
    --bg-start-scale: 1.1;
    --bg-end-tx:      0.5vw;
    --bg-end-ty:      4.5vh;
    --bg-end-scale:   1.15;
    --bg-tx:    calc(var(--bg-start-tx)    + var(--scroll-progress) * (var(--bg-end-tx)    - var(--bg-start-tx)));
    --bg-ty:    calc(var(--bg-start-ty)    + var(--scroll-progress) * (var(--bg-end-ty)    - var(--bg-start-ty)));
    --bg-scale: calc(var(--bg-start-scale) + var(--scroll-progress) * (var(--bg-end-scale) - var(--bg-start-scale)));
  }
  .teote-canvas {
    width:  max(100vw, calc(100vh * 6951 / 4805));
    height: max(100vh, calc(100vw * 4805 / 6951));
  }
}
