/* ─────────────────────────────────────────────────────────────────────────
   motion.css — homepage load reveal

   Reproduces Linear's hero cascade. All reference values were read off
   linear.app's Web Animations API on 2026-07-19 (getAnimations(), not video
   frames and not sampled inline styles — their reveal is WAAPI-driven, so
   inline styles just jump 0→1):

     text          filter + opacity + transform, 1000ms, 100ms line stagger
                   blur(10px) → 0, translateY(20%) → 0
     large visuals opacity ONLY, ~1500ms — no blur, no travel

   Two details are load-bearing and easy to get wrong:

   1. The text travel is a PERCENTAGE of the element's own height, not a pixel
      value, so it scales with the type down the breakpoints instead of
      becoming a slide-in on small screens.
   2. The heading is split per WORD and js/reveal.js groups words into rendered
      lines, so it resolves a line at a time at any breakpoint. Staggering
      within a line reads as dizzying word-by-word chatter; blurring the whole
      <h1> as one block reads flat.

   ── When it runs ──────────────────────────────────────────────────────────
   Gated on [data-reveal-state] on <html>, which an inline <head> script in
   index.html sets to "pending" — but only when the visitor arrived from
   OFF-SITE (external link, search, bookmark, typed URL). Any navigation from
   within this site is skipped, by link or by back button, so the reveal stays
   a first impression. See the note on that script for the full policy.

   That attribute is also the no-JS guard: it can only be set by script, so
   without JS no rule below matches and the page renders normally. It has to
   be decided in <head>, before first paint — deciding later would show a
   blank hero for a frame and then pop.

   ── prefers-reduced-motion ────────────────────────────────────────────────
   DECIDED 2026-07-19 (Leo): this cascade runs for EVERY visitor, including
   those who request prefers-reduced-motion: reduce. That matches linear.app,
   which was verified to animate in a reduce-mode context.

   This is a deliberate, informed exception, not an oversight — it was built
   the other way first (gated, with an opacity-only fade offered for reduce)
   and changed on request. Recording the tradeoff so nobody "fixes" it back
   without a decision:
     - blur + vertical movement are the specific triggers for vestibular
       motion sensitivity; opacity alone is not. A reduce-mode visitor gets
       the full treatment here.
     - the honoured-preference variant is a ~6-line change: wrap the state
       rules below in @media (prefers-reduced-motion: no-preference) and
       restore an early return in js/reveal.js.

   Because the reveal is not gated, it is live during VRT captures. settle()
   in vrt/capture.js waits for [data-reveal-state="done"] before screenshotting,
   so the harness stays deterministic by construction rather than by timing.
   ------------------------------------------------------------------------ */

/* All four values are the Linear measurements above. Per-item overrides go in
   the markup via --reveal-dur / --reveal-blur / --reveal-y / --reveal-gap
   (see the featured grid in index.html).

   --reveal-ease is plain `ease`, spelled out because the exact curve is the
   point: an expo-out like cubic-bezier(.16,1,.3,1) front-loads nearly all its
   movement into the first ~200ms and then crawls, which reads as choppy on a
   blur. This curve is gentler at both ends. */
:root {
  --reveal-dur: 1s;
  --reveal-ease: cubic-bezier(0.25, 0.1, 0.25, 1);
  --reveal-stagger: 100ms;
  --reveal-blur: 10px;
}

/* Only the heading's words need this: they are inline spans, and
   transform/filter have no effect on a non-replaced inline box. Everything
   else tagged as a reveal item is already block/flex/inline-block and must
   keep its own display, so this is deliberately NOT `[data-reveal-item]`.

   Applied unconditionally — inline-block must resolve identically whether or
   not the reveal runs, or a skipped navigation would lay out differently from
   an animated one. (Word boundaries are where text already breaks, so this
   does not move any line breaks.) */
[data-reveal-item="text"] {
  display: inline-block;
}

/* Hidden initial state. Matches any [data-reveal-state] value; the "in" and
   "done" rules below carry equal specificity (0-3-0) and win on source order.
   Per css/DECISIONS.md, don't "simplify" these selectors without re-checking
   the cascade. */
[data-reveal-state] [data-reveal] [data-reveal-item] {
  opacity: 0;
  filter: blur(var(--reveal-blur, 10px));
  transform: translateY(var(--reveal-y, 20%));
  transition:
    opacity   var(--reveal-dur, 1s) var(--reveal-ease),
    filter    var(--reveal-dur, 1s) var(--reveal-ease),
    transform var(--reveal-dur, 1s) var(--reveal-ease);
  /* --reveal-gap is an extra pause on top of the sequence position, for items
     that should read as a separate beat rather than the next tick of the
     stagger. Deliberately NOT declared on :root — it must not inherit, or it
     would delay every item equally and cancel itself out. */
  transition-delay: calc(var(--reveal-i, 0) * var(--reveal-stagger) + var(--reveal-gap, 0ms));
}

[data-reveal-state="in"] [data-reveal] [data-reveal-item] {
  opacity: 1;
  filter: blur(0px);
  transform: translateY(0%);
}

/* Post-cascade cleanup. Once every item has arrived, drop filter/transform
   altogether rather than leaving them parked at their identity values.
   `filter: blur(0px)` and `transform: translateY(0%)` are not free: they keep
   a containing block and a compositor layer alive for the life of the page,
   and they make text rasterise ~0.05% of pixels differently than untouched
   text — enough to keep the strict-0 VRT gate permanently red.

   transition:none prevents this from animating anything itself. */
[data-reveal-state="done"] [data-reveal] [data-reveal-item] {
  opacity: 1;
  filter: none;
  transform: none;
  transition: none;
}

/* Screen-reader-only copy of the split heading. The word-split copy is
   aria-hidden, so this carries the h1's accessible name as one clean string
   rather than ten disjoint fragments. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
