/* ============================================================================
   MOTION — the reveal primitive.

   One behaviour, shared by every section: a thing enters the viewport once and
   arrives. It never leaves again and it never re-fires. Everything else that
   moves on this site is a response to a pointer.

   THE JS GATE
   The resting (hidden) state hangs off `.js-reveal`, a class reveal.js puts on
   <html>. With JavaScript off — or if reveal.js throws on boot, or the visitor
   asked for reduced motion — the class never lands, the rest state never
   matches, and the page is simply visible. A reveal system that is capable of
   hiding content permanently will eventually do exactly that.

   ANIMATION, NOT TRANSITION
   The arrival is a keyframe animation rather than a transition because
   `transition` is winner-takes-all in the cascade: the moment a section gives
   one of its own cards a hover transition, one of the two declarations wins
   outright and the other silently disappears. Animations layer instead of
   fighting, so a section can own its hover and still be revealed.

   `translate`, NOT `transform`
   Same reason, one level down. The work slabs rest at a small rotation; a
   reveal that moved them with `transform` would flatten that tilt the instant
   it landed. The individual `translate` property composes with whatever
   `transform` a section owns, and neither has to know about the other.
   ========================================================================== */

/* Both hooks share one rest state: an element may ask for the block reveal,
   the line reveal, or — the hero h1 — carry both attributes. */
:is(.reveal-pending, .js-reveal) :is([data-reveal], [data-reveal-lines]):not(.is-in) {
  opacity: 0;
  translate: 0 var(--s4);
}

/* --i is written by reveal.js as the element's position among its revealing
   siblings, already capped. A row arrives as a row, not as a queue. */
.js-reveal :is([data-reveal], [data-reveal-lines]).is-in {
  animation: rv-in var(--t-reveal) var(--ease-out)
             calc(var(--stagger) * var(--i, 0)) both;
}

/* Landed. Hand the properties back to whoever owns the element: an animation
   left in `both` outranks every later declaration for the rest of the visit,
   and keeps a compositor layer alive for a thing that will never move again. */
.js-reveal :is([data-reveal], [data-reveal-lines]).is-done {
  animation: none;
  opacity: 1;
  translate: none;
}

@keyframes rv-in {
  from { opacity: 0; translate: 0 var(--s4); }
  to   { opacity: 1; translate: 0; }
}

/* ------------------------------------------------------------------- lines
   The hero h1, and nothing else. Each line is clipped by its own wrapper and
   the inner span rides up from under the clip, so the letterforms read as
   lifted off the page rather than faded onto it.

   The wrapper is padded and the padding is pulled straight back off with a
   matching negative margin. That buys the mask enough room for descenders and
   for the scribble hanging under "very seriously" WITHOUT changing the leading
   by a pixel — the two cancel exactly. The element becomes a flex column for
   one reason only: flex items do not collapse their margins, and collapsing
   would eat half of that compensation and tighten the hero. */
.js-reveal .rv-split {
  display: flex;
  flex-direction: column;
}

/* A split element hands its motion to its lines. Without this the block
   treatment would run underneath them and every line would move twice.
   Placed after the three rules above so equal specificity resolves this way. */
.js-reveal [data-reveal-lines].rv-split {
  animation: none;
  opacity: 1;
  translate: none;
}

.rv-line {
  /* Mask bleed, in em because it tracks the font, not the space ladder:
     headroom for accents above, descender room below. */
  --rv-over:  .16em;
  --rv-under: .22em;

  display: block;
  overflow: hidden;
  padding-block: var(--rv-over) var(--rv-under);
  margin-block: calc(var(--rv-over) * -1) calc(var(--rv-under) * -1);
}

.rv-line__in { display: block; }

/* Parked exactly on the clip edge — its own height plus the bleed — so not a
   pixel of the next line is visible before its turn.

   Gated on :not(.is-in), and that gate is the whole point. The obvious version
   of this rule parks the span unconditionally and relies on the animation below
   to bring it back, which means the headline's visibility depends on an
   animation actually running. It usually does. It does not in a paint-suppressed
   tab, under some throttling, or in a headless render — and the failure mode is
   not "no animation", it is a hero with no words in it. Tying the parked state
   to "has not been revealed yet" instead means the class landing is sufficient
   on its own, and the animation is decoration on top of a correct result. */
.js-reveal .rv-split:not(.is-in) .rv-line__in {
  translate: 0 calc(100% + var(--rv-under));
}

.js-reveal .rv-split.is-in .rv-line__in {
  animation: rv-line var(--t-reveal) var(--ease-out)
             calc(var(--stagger) * var(--i, 0)) both;
}

/* The 0% frame has to be explicit now: the rest state is no longer the parked
   offset, so an omitted first frame would resolve to translate:none and animate
   nothing to nowhere. */
@keyframes rv-line {
  from { translate: 0 calc(100% + var(--rv-under)); }
  to   { translate: 0; }
}

/* Once a line has landed the clip has no job left, and reveal.js releases it.
   Anything the mask bleed did not quite cover — a doodle drawn further below
   the baseline than expected — is whole again the moment the line stops. */
.rv-line.is-open { overflow: visible; }

/* --------------------------------------------------------------- reduced
   reveal.js never adds .js-reveal when reduced motion is set, so this only
   catches the visitor who flips the system setting mid-visit: whatever has not
   arrived yet is simply already there. */
@media (prefers-reduced-motion: reduce) {
  .reveal-pending :is([data-reveal], [data-reveal-lines]):not(.is-in),
  .js-reveal :is([data-reveal], [data-reveal-lines]):not(.is-in),
  .js-reveal :is([data-reveal], [data-reveal-lines]).is-in {
    animation: none;
    opacity: 1;
    translate: none;
  }
  .js-reveal .rv-split.is-in .rv-line__in { animation: none; translate: none; }
  .rv-line { overflow: visible; }
  .rv-line__in { translate: none; }
}

/* ----------------------------------------------------------------- failsafe
   The rest state of every reveal on this page is opacity:0, which is a fine
   bargain right up until the thing that undoes it never runs. reveal.js adds
   .rv-force when no reveal has landed within its grace period — a dead or
   undelivered IntersectionObserver, a non-compositing render, a print.

   This has to beat both treatments at once, including the line mask, whose
   parked translate is intentionally ungated so that a split headline stays
   hidden even before .js-reveal is applied. !important is load-bearing here
   rather than lazy: the rules it overrides are animations in their fill phase,
   and nothing short of it wins that. */
.rv-force :is([data-reveal], [data-reveal-lines]),
.rv-force :is([data-reveal], [data-reveal-lines]).is-in,
.rv-force .rv-split.is-in .rv-line__in,
.rv-force .rv-line__in {
  animation: none !important;
  opacity: 1 !important;
  translate: none !important;
  transform: none !important;
}
.rv-force .rv-line { overflow: visible; }
