/* ============================================================
   90 — MOTION

   Transitions for JS-triggered state classes. The JS never writes
   timing values; it only flips classes and sets the per-element
   custom properties that these rules read.
   ============================================================ */

/* ------------------------------------------------------------
   FADE IN  —  <FadeIn> from the reference design

   Every rule here is scoped to `.js` on <html>, and that scoping
   is load-bearing rather than stylistic.

   The starting state is opacity 0, revealed only when JS adds
   .is-in. If the scripts never run, an unscoped rule would leave
   the nav, headline, blurb and CTA permanently invisible — a
   blank page. That happens for real: ES modules are blocked by
   CORS on file://, so simply double-clicking index.html would
   show nothing at all.

   The `.js` class is set by an inline script in <head> (so there
   is no flash of visible-then-hidden content) and withdrawn again
   by a failsafe timer if the module never reports back. See
   index.html.
   ------------------------------------------------------------ */

.js [data-fade] {
  opacity: 0;
  transform: translate3d(var(--fade-x, 0px), var(--fade-y, 30px), 0);
  transition:
    opacity   var(--fade-dur, 0.7s) var(--ease) var(--fade-delay, 0s),
    transform var(--fade-dur, 0.7s) var(--ease) var(--fade-delay, 0s);
  will-change: opacity, transform;
}

/* Only the elements that actually rotate on entry pay for a
   perspective. Applying it to every faded element would push all
   72 of them into their own 3D rendering context for no visual
   gain. perspective() sits in the element's own transform rather
   than on a container — see 91-depth.css for why that matters. */
.js [data-fade][data-fade-rotate] {
  transform:
    perspective(var(--perspective))
    translate3d(var(--fade-x, 0px), var(--fade-y, 30px), 0)
    rotateX(var(--fade-rx, 0deg));
}

/* Resets the variables rather than setting `transform: none`.
   Same visual result, but it leaves the transform expression
   intact so other effects layered on the element keep working —
   see the composition rule in 91-depth.css. */
.js [data-fade].is-in {
  opacity: 1;
  --fade-x: 0px;
  --fade-y: 0px;
  --fade-rx: 0deg;
  will-change: auto;
}

/* ------------------------------------------------------------
   ANIMATED TEXT  —  character-by-character scroll reveal

   Each character sits at 0.2 opacity and is lifted to 1 as the
   scroll progress passes its position in the string. No transition:
   opacity is written directly from the scroll position every frame,
   so the reveal tracks the scrollbar rather than running on a timer.
   ------------------------------------------------------------ */

.js .char {
  opacity: 0.2;
  will-change: opacity;
}

/* ------------------------------------------------------------
   REDUCED MOTION

   Everything resolves to its final state immediately. The scroll
   engine and magnet bail out entirely in JS; these rules cover the
   CSS-driven half.
   ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  [data-fade],
  [data-fade].is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .char {
    opacity: 1;
  }
}
