/* ============================================================================
   Floom Motion — scroll reveals, scroll-linked parallax, reduced-motion guard
   ----------------------------------------------------------------------------
   Drop-in: <link rel="stylesheet" href="/assets/floom-motion.css">
            <script src="/assets/floom-motion.js" defer></script>

   Design split (deliberate):
     • Reveals   → one-shot, driven by IntersectionObserver. Zero per-frame cost.
     • Parallax  → continuous, driven by CSS scroll-driven animation where the
                   browser supports it (runs on the compositor, 0ms main thread),
                   with an rAF fallback everywhere else.

   Only `opacity` and `transform` are animated, so every transition is
   compositor-driven and holds 60fps+ without layout or paint work.
   ========================================================================== */

:root{
  /* Expo-out: almost all the distance is covered early, then a long quiet
     settle. That tail is what reads as "premium" — the reference never shows
     an element arriving abruptly, and the motion blur across its text means
     the travel is unhurried rather than snappy. */
  --fl-ease:         cubic-bezier(.16, 1, .3, 1);
  --fl-reveal-dur:   900ms;
  --fl-stagger:      110ms;
  --fl-shift:        32px;
  /* Parallax amplitude multiplier. 1 on desktop; the breakpoints below scale
     it so per-element `--fl-par-from` tuning keeps working at every width. */
  --fl-par-scale:    1;
}

/* ── Reveals ───────────────────────────────────────────────────────────────
   Hidden until JS adds .is-revealed. `.fl-motion` is set on <html> by the
   script, so if JS fails or is blocked the content stays visible — never a
   blank page behind a broken observer.                                      */

.fl-motion [data-reveal]{
  opacity:0;
  transition:
    opacity   var(--fl-reveal-dur) var(--fl-ease) var(--fl-delay, 0ms),
    transform var(--fl-reveal-dur) var(--fl-ease) var(--fl-delay, 0ms),
    filter    var(--fl-reveal-dur) var(--fl-ease) var(--fl-delay, 0ms);
}

/* Stagger: JS sets --fl-i (child index); delay is derived in CSS. */
.fl-motion [data-reveal]{ --fl-delay: calc(var(--fl-i, 0) * var(--fl-stagger)); }

.fl-motion [data-reveal="up"]    { transform: translate3d(0, var(--fl-shift), 0); }
.fl-motion [data-reveal="down"]  { transform: translate3d(0, calc(var(--fl-shift) * -1), 0); }
.fl-motion [data-reveal="left"]  { transform: translate3d(calc(var(--fl-shift) * -1), 0, 0); }
.fl-motion [data-reveal="right"] { transform: translate3d(var(--fl-shift), 0, 0); }
.fl-motion [data-reveal="scale"] { transform: scale(.965); }
.fl-motion [data-reveal="fade"]  { transform: none; }
.fl-motion [data-reveal="media"] {
  transform: translate3d(0, var(--fl-shift), 0) scale(.975);
  filter: blur(6px);
}

/* Promote to its own layer only while the reveal is actually pending —
   a permanent will-change on dozens of nodes wastes GPU memory. */
.fl-motion [data-reveal]:not(.is-revealed){ will-change: opacity, transform; }

.fl-motion [data-reveal].is-revealed{
  opacity:1;
  transform:none;
  filter:none;
  will-change:auto;
}

/* ── Parallax ──────────────────────────────────────────────────────────────
   Opt in with data-parallax. Tune per element:
     style="--fl-par-from:34px; --fl-par-to:-34px"
   The element must not carry another transform of its own — this owns it.   */

[data-parallax]{ will-change: transform; }

@supports (animation-timeline: view()){
  [data-parallax]{
    animation: fl-parallax linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }
  /* Native path needs no rAF at all — drop the JS fallback's hook. */
  [data-parallax]{ --fl-par-native: 1; }
}

/* ── Horizontal word reveal ────────────────────────────────────────────────
   Opt in with data-hreveal on a block of copy. The script splits it into words
   and drives them per frame; these are the only rules they need. The words are
   laid out where they finally belong, so the block's final state is byte for
   byte the page without this effect — and its height never changes. */

[data-hreveal] span.fl-hrw{ display: inline-block; }
[data-hreveal] .fl-hrw{ will-change: transform, opacity; }

@keyframes fl-parallax{
  from { transform: translate3d(0, calc(var(--fl-par-from,  40px) * var(--fl-par-scale, 1)), 0); }
  to   { transform: translate3d(0, calc(var(--fl-par-to,   -40px) * var(--fl-par-scale, 1)), 0); }
}

/* ── Intensity by device ───────────────────────────────────────────────────
   One token set, three intensities. Nothing is re-implemented per breakpoint:
   the travel distances shrink and the durations tighten, so the animation
   language is identical everywhere and only its amplitude changes. The
   breakpoints are the site's own — FloomFX.DESKTOP is (min-width: 901px), and
   600px is where the layout goes single-column.

   Parallax is switched OFF below 601px rather than merely reduced. It is
   continuous work tied to a scroll the user drives with their thumb, and a
   phone viewport is short enough that a 40px offset is a large fraction of an
   element's travel on screen — it stops reading as depth and starts reading
   as drift. Reveals stay: they are one-shot and cost nothing per frame.      */

@media (max-width: 900px){
  :root{
    --fl-shift:      22px;    /* was 32 */
    --fl-reveal-dur: 760ms;   /* was 900 */
    --fl-stagger:    80ms;    /* was 110 */
  }
  /* Parallax travel is SCALED, not replaced. Every parallax element on the
     site tunes itself inline (`style="--fl-par-from:20px"`), and an inline
     property beats a rule here — so setting --fl-par-from in this block would
     reduce nothing on exactly the elements that exist. The scale is a separate
     token the keyframe multiplies by, so per-element tuning composes with it. */
  :root{ --fl-par-scale: .55; }
}

@media (max-width: 600px){
  :root{
    --fl-shift:      16px;
    --fl-reveal-dur: 640ms;
    --fl-stagger:    60ms;
  }
  /* Off, not small: a residual few-px slide is the worst of both — it still
     costs a composited layer per element and reads as a rendering fault. */
  :root{ --fl-par-scale: 0; }
  [data-parallax]{
    animation: none !important;
    transform: none !important;
    will-change: auto;
  }
}

/* ── Reduced motion ────────────────────────────────────────────────────────
   Matches the guard already used on the blog templates. Everything lands in
   its final state instantly; nothing loops; anchor jumps stop animating.
   HIG — Motion: "Make motion optional."
   HIG — Accessibility: reduce automatic and repetitive animation.           */

@media (prefers-reduced-motion: reduce){
  html{ scroll-behavior: auto !important; }

  *, *::before, *::after{
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }

  .fl-motion [data-reveal],
  .fl-motion [data-reveal].is-revealed{
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;          /* HIG: don't animate into or out of blur */
    will-change: auto !important;
  }

  [data-hreveal] .fl-hrw{
    transform: none !important;
    opacity: 1 !important;
  }
  [data-parallax]{
    animation: none !important;
    transform: none !important;
    will-change: auto !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   Sections that slide over one another
   --------------------------------------------------------------------------
   The outgoing section holds while the next one rises over it, which is what
   the reference does. Sticky, not a transform: a transformed ancestor becomes
   the containing block for every fixed and sticky descendant, so moving these
   with transform would break the two sections that own their own pins.

   `top` is written per section by floom-motion.js, not set here. A section
   taller than the window cannot pin at 0 — its bottom would never be reached
   before the next one covered it — so a tall one pins at
   `window height - its own height`, which holds it only once all of it has
   been seen. That number is not expressible in CSS: a % in `top` resolves
   against the containing block, never against the element itself.

   An opaque background is the other half of it. A section with a transparent
   ground shows the pinned one straight through, which is why #questions is
   not in the set — the section that follows it has none.
   ========================================================================== */
/* `section[...]` and not the bare attribute: .st and .st-band already set
   `position: relative`, they live in a stylesheet loaded after this one, and
   at equal specificity the later rule wins — three of the five sections came
   out `relative` and simply scrolled away. The element name takes this to
   (0,1,1) and settles it. */
section[data-fl-over]{
  position: sticky;
  top: 0;
}

/* The run's container. A sticky section never leaves its parent's box, so the
   held sections release when this box's end scrolls up instead of staying
   pinned to the end of the page behind every later section, and any later
   section with a see-through ground would show them through itself. */
.fl-over{ position: relative; }

/* Going back (floom-motion.js writes --fl-recede, 0 -> 1, eased). The ground
   stays where it is; the work on it goes back — 8% smaller, a little higher,
   faded out — about the middle of the window, which is the part of a held
   section still showing. Only while it is happening: an identity transform
   would still make every child a containing block for nothing. */
[data-fl-recede].is-receding > *{
  transform: translateY(calc(var(--fl-recede, 0) * -4vh)) scale(calc(1 - var(--fl-recede, 0) * .08));
  transform-origin: 50% calc(100% - 50vh);
  opacity: calc(1 - var(--fl-recede, 0) * .94);
}

/* A dark section the whole page goes into (floom-motion.js adds .has-veil
   to <html>, .is-veiled while the section holds the middle of the window,
   and .is-veiling on the section). The veil sits over every section and
   under the fixed controls (.fl-way 80, the menus 89–95, the nav 100); the
   section rises over it with its ground cleared, so the veil IS its ground
   and there is no edge anywhere on the screen. Without the script, or under
   reduced motion, none of this applies and the section keeps its own. */
[data-fl-dark]{ --fl-veil: var(--fl-ink); }
.fl-veil{
  position: fixed;
  inset: 0;
  z-index: 40;
  background: var(--fl-veil, var(--fl-ink));
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  /* Going: the section's content has left first, then the page comes back. */
  transition: opacity 760ms cubic-bezier(.45, 0, .25, 1) 140ms, visibility 0s linear 900ms;
}
.is-veiled .fl-veil{
  opacity: 1;
  visibility: visible;
  transition: opacity 760ms cubic-bezier(.45, 0, .25, 1), visibility 0s;
}
.has-veil [data-fl-dark]{
  position: relative;
  z-index: 41;
  background: transparent;
}
/* Its content arrives once the dark is well on its way, and leaves before
   the cream comes back, so nothing light is ever seen on a light ground. */
.has-veil [data-fl-dark] > *{
  opacity: 0;
  transition: opacity 300ms ease;
}
.has-veil [data-fl-dark].is-veiling > *{
  opacity: 1;
  transition: opacity 620ms cubic-bezier(.45, 0, .25, 1) 300ms;
}
/* A dark section shorter than the window cannot be the screen, so it is not
   veiled (floom-motion.js marks it .is-solo): it keeps its own ground and its
   content all the time, like any other section. Without this, on a phone the
   closing CTA was a see-through gap until it reached the middle. */
.has-veil [data-fl-dark].is-solo{ background: var(--fl-veil, var(--fl-ink)); }
.has-veil [data-fl-dark].is-solo > *{ opacity: 1; transition: none; }

@media (prefers-reduced-motion: reduce){
  [data-fl-recede].is-receding > *{ transform: none; opacity: 1; }
  /* The hold is the motion. Without it they are ordinary sections. */
  section[data-fl-over]{ position: relative; top: auto; }
}
