/* fx.css: depth planes, cut-outs, grain. Owned separately from the UI (styles.css). */
/* ==========================================================================
   DEPTH PLANES (2.5D). Each scene carries its clip's camera vector on the section:
     --tvx / --tvy  how far the world slides across the scene (px)    --tvz  how much it zooms (dolly)
   Every plane moves along that vector scaled by its depth --d: far planes barely move, near planes rush.
   Order inside a stage, back to front:
     clip canvas -> .fx.far -> .fx.mid -> tags (.cover.mid) -> foreground matte (.fg) -> tags (.cover.front)
     -> .fx.near -> .cut (out-of-focus foreground lifted from the clips)
   ========================================================================== */
/* every layer can carry an opacity envelope over scene progress: in from --fa..--fb, out from --fc..--fd */
.fx, .cut {
  --env: calc(clamp(0, (var(--p) - var(--fa, -2)) / (var(--fb, -1) - var(--fa, -2)), 1)
            * clamp(0, (var(--fd, 3) - var(--p)) / (var(--fd, 3) - var(--fc, 2)), 1));
}
.fx {
  position: absolute; inset: -30%; pointer-events: none;
  opacity: calc(var(--o, .8) * var(--env));
  transform:
    translate3d(calc((var(--p) - .5) * var(--d) * var(--tvx, 0px)), calc((var(--p) - .5) * var(--d) * var(--tvy, 0px)), 0)
    scale(calc(1 + var(--p) * var(--d) * var(--tvz, 0)));
  will-change: transform;
}
/* the sheet itself drifts by exactly one tile on a loop, so the plane is alive even when scrolling stops.
   It is padded by one tile only on the side it drifts away from (fx.js sets --dt/--dr/--db/--dl to 0 or 1):
   padding every side made near planes thousands of px larger than the screen, which costs hundreds of
   MB of graphics memory on a 3x phone. */
.fx > i {
  position: absolute;
  inset: calc(-1 * var(--ts) * var(--dt, 0)) calc(-1 * var(--ts) * var(--dr, 0)) calc(-1 * var(--ts) * var(--db, 0)) calc(-1 * var(--ts) * var(--dl, 0));
  background: var(--img) repeat 0 0 / var(--ts) var(--ts);
  animation: fx-drift var(--dur, 40s) linear infinite;
  will-change: transform;
}
@keyframes fx-drift { to { transform: translate3d(calc(var(--ix, 0) * var(--ts)), calc(var(--iy, 0) * var(--ts)), 0); } }
.fx.far  { --d: .25; --ts: 380px; }
.fx.mid  { --d: .6;  --ts: 720px; }
.fx.near { --d: 1.3; --ts: 1400px; --o: .9; }

/* a cut: one piece of art (out-of-focus silhouette, light shaft, haze band, flare, lens dirt) travelling its own
   path across a window [--a, --b] of the scene: position x0->x1, y0->y1, scale s0->s1, rotation r0->r1,
   opacity o0->o1. All of it is transform + opacity on a pre-blurred image: no filters, no blend modes. */
.cut {
  position: absolute; left: 0; top: 0; pointer-events: none;
  --k: clamp(0, calc((var(--p) - var(--a, 0)) / (var(--b, 1) - var(--a, 0))), 1);
  transform-origin: var(--org, 50% 50%);
  transform:
    translate3d(calc(var(--x0, 0px) + (var(--x1, var(--x0, 0px)) - var(--x0, 0px)) * var(--k)),
                calc(var(--y0, 0px) + (var(--y1, var(--y0, 0px)) - var(--y0, 0px)) * var(--k)), 0)
    rotate(calc(var(--r0, 0deg) + (var(--r1, var(--r0, 0deg)) - var(--r0, 0deg)) * var(--k)))
    scale(calc(var(--s0, 1) + (var(--s1, 1) - var(--s0, 1)) * var(--k)))
    scaleX(var(--flip, 1));
  opacity: calc((var(--o0, 1) + (var(--o1, var(--o0, 1)) - var(--o0, 1)) * var(--k)) * var(--env));
  will-change: transform, opacity;
}
.cut > img { display: block; height: var(--h, auto); width: var(--w, auto); max-width: none; }

/* idle life on the art itself, so nothing is frozen when scrolling stops (slow, small, compositor-only) */
.cut > .sway    { transform-origin: var(--swo, 50% 0); animation: fx-sway var(--swd, 9s) ease-in-out var(--del, 0s) infinite alternate; }
.cut > .bob     { animation: fx-bob var(--swd, 12s) ease-in-out var(--del, 0s) infinite alternate; }
.cut > .slide   { animation: fx-slide var(--swd, 30s) ease-in-out var(--del, 0s) infinite alternate; }
.cut > .pulse   { animation: fx-pulse var(--swd, 7s) ease-in-out var(--del, 0s) infinite alternate; }
.cut > .flicker { animation: fx-flicker var(--swd, 3.4s) linear infinite; }
/* speed wisps: the radial sheet swells out of the vanishing point and fades, over and over (two copies offset) */
.cut > .rush    { transform-origin: 50% 46%; animation: fx-rush var(--swd, 1.5s) linear var(--del, 0s) infinite; }
@keyframes fx-rush {
  0% { transform: scale(.78); opacity: 0; } 35% { opacity: 1; } 100% { transform: scale(1.35); opacity: 0; }
}
@keyframes fx-sway  { from { transform: rotate(calc(-1 * var(--sw, 1.2deg))); } to { transform: rotate(var(--sw, 1.2deg)); } }
@keyframes fx-bob   { from { transform: translate3d(0, calc(-1 * var(--sw, 1vh)), 0); } to { transform: translate3d(0, var(--sw, 1vh), 0); } }
@keyframes fx-slide { from { transform: translate3d(calc(-1 * var(--sw, 4vw)), 0, 0); } to { transform: translate3d(var(--sw, 4vw), 0, 0); } }
@keyframes fx-pulse { from { opacity: 1; } to { opacity: .55; } }
@keyframes fx-flicker {
  0% { opacity: .85; } 13% { opacity: 1; } 27% { opacity: .78; } 41% { opacity: .94; } 55% { opacity: .82; }
  68% { opacity: 1; } 84% { opacity: .74; } 100% { opacity: .85; }
}

/* the card lives on a plane nearer than the thing it's pinned to: it drifts further along the camera
   vector than its anchor, and the leader stretches between the two planes */
.tag { --pz: .55; }
.cover.front .tag { --pz: .9; }
.card {
  --psx: calc((var(--p) - .5) * var(--pz) * var(--tvx, 0px) * .32);
  --psy: calc((var(--p) - .5) * var(--pz) * var(--tvy, 0px) * .32);
  translate: calc(var(--cx, 0%) + var(--psx)) calc(var(--cy, -100%) + var(--psy));
}
.leader {
  width: hypot(calc(var(--dx) + var(--psx)), calc(var(--dy) + var(--psy)));
  rotate: atan2(calc(var(--dy) + var(--psy)), calc(var(--dx) + var(--psx)));
}
.tag { --psx: calc((var(--p) - .5) * var(--pz) * var(--tvx, 0px) * .32); --psy: calc((var(--p) - .5) * var(--pz) * var(--tvy, 0px) * .32); }

/* scroll-velocity grain: rises when you move fast, settles when you stop (JS sets --vel on it) */
.grain {
  position: fixed; inset: -10%; z-index: 10; pointer-events: none;
  background: url("assets/fx/grain.webp") repeat 0 0 / 256px 256px;
  mix-blend-mode: overlay;
  opacity: calc(.04 + var(--vel, 0) * .16);
  animation: grain .9s steps(6) infinite;
}
@keyframes grain {
  0% { transform: translate3d(0, 0, 0); } 20% { transform: translate3d(-7%, 4%, 0); } 40% { transform: translate3d(5%, -6%, 0); }
  60% { transform: translate3d(-3%, 8%, 0); } 80% { transform: translate3d(8%, 2%, 0); } 100% { transform: translate3d(0, 0, 0); }
}

/* UI picks up each scene's light: --tint is an "r g b" triplet set per scene */
.stage { --tint: 95 227 201; }

/* portrait / caption-card mode: drop the secondary layers, keep the scene's main depth cues */
html.cards .nc { display: none; }
/* phone lite mode: phones have the least graphics memory, so planes are static, tighter, and only the
   far/mid ones remain; no near planes, extra sheets or grain */
html.cards .fx { inset: -10%; }
html.cards .fx > i { animation: none; inset: 0; }
html.cards .fx.near, html.cards .fx.extra, html.cards .grain { display: none; }
html:not(.cards) .mo { display: none; }   /* portrait-only stand-ins (smaller, pushed to the edges) */

/* a scene's static colour grade (fx.json "grade"): plain alpha over the clip, never animated */
.grade { position: absolute; inset: 0; pointer-events: none; background: var(--gbg); }
html.cards .grade { background: var(--gbgm, var(--gbg)); }

@media (prefers-reduced-motion: reduce) {
  .fx > i, .grain, .cut > img, .cut > [class] { animation: none; }
  .fx.near, .fx.extra, .cut, .grain { display: none; }
  .cut.keep { display: block; }   /* static light (shafts, glows) stays: it doesn't move on its own */
}

