/* ── Motion ─────────────────────────────────────────────────────────────
   The dolly move lives in the video. This file adds the z-axis cue that
   makes a room change read as *entering* rather than *scrolling*:
   outgoing copy grows past you, incoming copy arrives from behind.

   Direction comes from .stage[data-dir]; phase from .copy[data-state].
*/
.copy{
  transition:opacity var(--t-copy) linear, transform var(--t-copy) var(--ease);
}
.copy[data-state="settled"]{opacity:1;transform:none}

.stage[data-dir="in"]  .copy[data-state="leaving"] {opacity:0;transform:scale(1.08)}
.stage[data-dir="in"]  .copy[data-state="entering"]{opacity:0;transform:scale(.94)}
.stage[data-dir="out"] .copy[data-state="leaving"] {opacity:0;transform:scale(.94)}
.stage[data-dir="out"] .copy[data-state="entering"]{opacity:0;transform:scale(1.08)}

/* ── Crossfade — the fallback path ──────────────────────────────────────
   Runs whenever no clip plays: the clip does not exist yet, the room jump
   is not to a neighbour, reduced motion is asked for, autoplay is refused,
   or the file failed to load.

   Only the INCOMING layer animates. Stacking two half-opaque images would
   let --ground bleed through the middle of the dissolve — at t=0.5 the
   composite is 25% background, a visible dip to black. Holding the
   outgoing layer fully opaque underneath makes it a true cross-dissolve:
       out = I_next·t + I_current·(1−t)
*/
.still--current{opacity:1}
.still--next{
  opacity:0;
  transition:opacity var(--t-cross) linear, transform var(--t-cross) var(--ease);
}

/* Pre-arm the incoming frame so its push starts from the correct side. */
.stage[data-dir="in"]  .still--next{transform:scale(.96)}
.stage[data-dir="out"] .still--next{transform:scale(1.06)}

.stage[data-mode="crossfade"] .still--next{opacity:1;transform:scale(1)}

/* The outgoing layer only ever moves — never fades.
   No first-paint fade and no JS gate: the hero paints the moment the browser
   decodes it. Holding it transparent until a deferred script ran made the
   photograph arrive LATER and then jump, which is worse than the flash it
   was meant to prevent. The page background is --ground, the same tone the
   image sits on, so there is nothing to hide in the first place. */
.still--current{transition:transform var(--t-cross) var(--ease)}
.stage[data-mode="crossfade"][data-dir="in"]  .still--current{transform:scale(1.06)}
.stage[data-mode="crossfade"][data-dir="out"] .still--current{transform:scale(.96)}

/* ── Reset frame ────────────────────────────────────────────────────────
   After a dissolve, app.js copies the target into the current layer and
   drops the incoming one back to zero. Both layers hold identical pixels
   at that instant, so the swap must be INSTANT — animating it would
   replay the dissolve against itself. app.js adds .no-anim, forces a
   reflow, then removes it.                                               */
.stage.no-anim .still,
.stage.no-anim .copy{transition:none!important}

/* ── Reduced motion ─────────────────────────────────────────────────────
   Not a downgrade — a different, complete experience. Clips never play
   (app.js reads the same query), and the dissolve keeps a short fade so
   the room change is still perceivable, but loses every scale push.      */
@media (prefers-reduced-motion:reduce){
  .copy{transition-duration:.01ms}
  .copy[data-state="leaving"],
  .copy[data-state="entering"]{transform:none}

  .still--next{transition:opacity 200ms linear}
  .still,
  .stage[data-dir="in"] .still--next,
  .stage[data-dir="out"] .still--next,
  .stage[data-mode="crossfade"] .still--current{transform:none!important}
}
