/* ==========================================================================
   Outflo — motion layer

   🔴 THE LOAD-BEARING RULE
   Every animated initial state in the build lives in this file, and every
   selector here is scoped to `html.motion`. That class is set by reveal.js on
   its first line, and is NOT set when the browser reports
   prefers-reduced-motion: reduce.

   Consequences, both deliberate:
     - No script  -> no `.motion` class -> nothing is hidden. The page reads.
     - Delete this file -> the page reads. Motion is purely additive.

   An earlier build hid every section below the hero when its scroll timeline
   failed to resolve. This structure is why that cannot happen again.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. The shared reveal — one treatment, not a bespoke animation per section.
      js/reveal.js sets --i (the stagger index) and adds .in on intersection.
   -------------------------------------------------------------------------- */
html.motion [data-reveal]{
  opacity:0;
  transform:translate3d(0,18px,0);
  transition:
    opacity .62s var(--ease-out) calc(var(--i,0) * 70ms),
    transform .62s var(--ease-out) calc(var(--i,0) * 70ms);
}
html.motion [data-reveal].in{
  opacity:1;
  transform:none;
}

/* --------------------------------------------------------------------------
   2. Named moment 1 — the headline types itself.

      This rule only holds the line invisible for the frame between paint and
      hero.js taking over. hero.js removes the class the moment it has either
      wrapped the characters or decided it cannot, and its 900ms timeout is the
      guarantee: if the script is slow or blocked, the headline just appears.

      🔴 The per-character state below is scoped to `html.hd-on`, which hero.js
      adds ONLY after the wrap has succeeded — so it can never hide text that
      nothing is going to reveal. Same contract as `.tw-on` in section 7.
   -------------------------------------------------------------------------- */
html.motion .d1.pre-split{
  opacity:0;
}

html.hd-on .d1 .ch{
  opacity:0;
  /* A 60ms ramp, shorter than the opener's 90ms because the headline runs at
     22ms/char: a longer fade than the gap between characters smears the
     leading edge into a gradient instead of a caret. */
  transition:opacity 60ms linear;
}
/* 🔴 `position:relative` belongs on the CARET, not on every character.
   It was on `.ch`, which made a containing block out of all 61 of them — and
   490 across the page — for the sake of one absolutely-positioned ::after that
   only ever exists on a single character at a time. Only one element needs it,
   and giving it only to that one costs nothing: `position:relative` with no
   offsets moves nothing, so the caret lands in exactly the same place. */
html.hd-on .d1 .ch.caret{position:relative}
html.hd-on .d1 .ch.on{opacity:1}
/* Spaces carry no ink, so fading them is work with nothing to show. */
html.hd-on .d1 .ch--sp{transition:none}

/* The caret, sized in `em` so it tracks the clamped headline rather than
   staying a hairline at 3.8rem. `currentColor` carries it from cream on the
   claim to blue on the question with no second rule. */
html.hd-on .d1 .ch.caret::after{
  content:'';
  position:absolute;
  right:-.06em;top:.04em;bottom:.1em;
  width:.045em;
  background:currentColor;
  animation:tw-blink .9s steps(1,end) infinite;
}

/* Finished. hero.js now restores the headline's ORIGINAL markup at this point,
   so there are normally no `.ch` elements left for this rule to match — it is
   kept as the belt to that braces: if the restore ever failed, the characters
   must still end up visible and inert rather than mid-transition. */
html.hd-done .d1 .ch{
  transition:none;
  opacity:1 !important;
}

/* --------------------------------------------------------------------------
   3. Named moment 2 — the flow tail.

      The tail is a FILLED variable-width path, not a stroke, so it cannot be
      drawn with DrawSVG (see docs/DESIGN.md → DrawSVG is dropped). It is
      revealed by translating a clipPath rect across it, left to right, which
      reads as flow rather than as drafting.

      The rect starts fully to the left of the tail's bounding box. If script
      never runs, `.motion` is absent, the transform below never applies, and
      the tail is simply present.
   -------------------------------------------------------------------------- */
html.motion .tail-wipe{
  transform:translateX(-460px);
  /* The transition lives on the BASE rule, not only on .in. With it only on
     .in, pointer.js's hover-replay (which removes .in for two frames) snapped
     the tail out of existence and then wiped it back — a visible pop. The
     reset is now a fast wipe-out, so the replay reads as one gesture. */
  transition:transform .22s var(--ease-out);
}
html.motion .tail-wipe.in{
  transform:translateX(0);
  transition:transform .6s var(--ease-out);
}

/* Mark and wordmark in the nav arrive with the page, not with the scroll. */
html.motion .nav-in{
  opacity:0;
  transform:translate3d(0,-10px,0);
  transition:opacity .5s var(--ease-out) .12s,transform .5s var(--ease-out) .12s;
}
html.motion .nav-in.in{opacity:1;transform:none}

/* --------------------------------------------------------------------------
   4. will-change hygiene — set immediately before a moment, removed after.
      reveal.js adds .settled once its transition has ended.
   -------------------------------------------------------------------------- */
html.motion [data-reveal].settled{
  will-change:auto;
}

/* --------------------------------------------------------------------------
   5. Belt and braces. `.motion` is not applied under reduced motion, so this
      block should be unreachable — it exists because a media query is cheaper
      than trusting one line of JavaScript with the whole page.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion:reduce){
  html.motion [data-reveal],
  html.motion .d1.pre-split,
  html.hd-on .d1 .ch,
  html.motion .nav-in,
  html.motion .tail-wipe{
    opacity:1 !important;
    transform:none !important;
    filter:none !important;
    transition:none !important;
    animation:none !important;
  }
}

/* --------------------------------------------------------------------------
   6. The funnel rows. The BARS these rules used to animate were deleted — see
      styles.css: a log-scaled bar at this dynamic range undersold the very
      claim it existed to support. The rows keep their staggered lift, which is
      what made the four read as a sequence collapsing.
   -------------------------------------------------------------------------- */
html.motion .funnel-row[data-reveal]{
  transition:
    opacity .6s var(--ease-out) calc(var(--i,0) * 70ms),
    transform .6s var(--ease-out) calc(var(--i,0) * 70ms);
}

/* --------------------------------------------------------------------------
   7. The opener types itself.

   🔴 Scoped to `html.tw-on`, which hero.js adds ONLY after it has
   successfully wrapped every character. No JS, reduced motion, or a thrown
   split all leave the class absent and the email fully present. Nothing here
   can hide the text on its own.

   Every character is wrapped once and revealed with `opacity` — never inserted
   — so the paragraph's line breaks and the panel's height are final on the
   first frame. A typewriter that appends characters reflows the largest element
   in the hero on every keystroke.

   Beat 3 of the sequence: it starts when the headline reports finished, not on
   a timer of its own. See hero.js.
   -------------------------------------------------------------------------- */
html.tw-on .mail-body .ch{
  opacity:0;
  /* A 90ms fade rather than a hard switch: on a high-contrast serif an instant
     flick reads as a glitch, and the short ramp gives the leading edge a soft
     frontier that looks like writing rather than like a cursor stamping. */
  transition:opacity 90ms linear;
}
html.tw-on .mail-body .ch.on{opacity:1}
/* Spaces carry no ink, so fading them is work with nothing to show. */
html.tw-on .mail-body .ch--sp{transition:none}

/* The caret. Sits on the character just revealed, so it needs no positioning
   from JS and cannot fall out of sync with the text. The keyframes are shared
   with the headline's caret in section 2. Scoped to `.caret` for the reason
   given in section 2: one character needs a containing block, not 429. */
html.tw-on .mail-body .ch.caret{position:relative}
html.tw-on .mail-body .ch.caret::after{
  content:'';
  position:absolute;
  right:-0.09em;top:.06em;bottom:.06em;
  width:1.5px;
  background:var(--blue-deep);
  animation:tw-blink .9s steps(1,end) infinite;
}
@keyframes tw-blink{50%{opacity:0}}

/* The researched sentence's wash is an annotation ON the line, so it arrives
   after the line exists. hero.js adds .on to the .hl when its last
   character lands. */
html.tw-on .mail-body .hl{
  background:transparent;
  box-shadow:none;
  transition:background-color .45s var(--ease-out),box-shadow .45s var(--ease-out);
}
html.tw-on .mail-body .hl.on{
  background:var(--blue-wash);
  box-shadow:0 0 0 4px var(--blue-wash);
}

/* Once it has finished, hero.js restores the opener's ORIGINAL markup — which
   removes the spans outright, brings back the <mark> on the researched sentence
   and the <br> in the signature, and returns the email to the accessibility
   tree. This rule normally matches nothing and exists for the same reason as
   its counterpart in section 2: a failed restore must still leave text visible. */
html.tw-done .mail-body .ch{
  transition:none;
  opacity:1 !important;
}

@media (prefers-reduced-motion:reduce){
  html.tw-on .mail-body .ch{opacity:1 !important;transition:none !important}
  html.tw-on .mail-body .ch.caret::after,
  html.hd-on .d1 .ch.caret::after{display:none}
  html.tw-on .mail-body .hl{background:var(--blue-wash);box-shadow:0 0 0 4px var(--blue-wash)}
}
