/* ==========================================================================
   VP Design System — Motion
   --------------------------------------------------------------------------
   FOUR signature patterns. Every fold on index.html reuses this vocabulary.
   Nothing here is invented per-section; a new section composes these, it does
   not add a fifth pattern. See docs/motion/motion-system.md.

     M1  ENTRANCE   .fade-reveal / .line-reveal-* / .word-reveal-span
     M2  AMBIENT    vp-gradient-drift, vp-shimmer, vp-star-spin, liquid canvas
     M3  INTERACT   .pill-btn hover chain, .fade-overlay dim, .link-swap
     M4  SCROLL     .sticky-scene 200vh pin + .border-top/.border-bottom wipe

   Provenance:
     M1        — Lumora index.html, class names + curves adopted verbatim.
     M2 loops  — textura.agency: @keyframes gradientAnimation / shimmer /
                 starAnimation. Mechanism ported 1:1, colour stops re-tuned.
     M3 dim    — textura.agency `.lbdlSd + .fade-overlay` and `.lbdlSd .star`.
     M3 swap   — textura.agency `.gHsQCg .link .first/.second`.
     M4        — textura.agency `.fLxUCv` / `.fLxUCv .sticky` / `.border-top`.
   ========================================================================== */


/* ==========================================================================
   M1 — ENTRANCE
   Three grains of the same idea: a thing arrives from below and settles.
   Choose by content size: fade = block, line = heading line, word = sentence.
   All three are inert until JS adds `.revealed`, which is why base.css only
   shortens their duration under prefers-reduced-motion instead of disabling
   it — a disabled transition still lands on the revealed state.
   ========================================================================== */

/* M1a — block fade. The workhorse: every card, row, CTA group. */
.fade-reveal {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity   var(--duration-fade) var(--ease-soft),
    transform var(--duration-fade) var(--ease-soft);
  will-change: opacity, transform;
}
.fade-reveal.revealed { opacity: 1; transform: translateY(0); }

/* M1b — masked line. For display headings only: the wrap clips, the inner
   slides its own full height. Reads as type being set, not as type fading. */
.line-reveal-wrap { overflow: hidden; display: block; }
.line-reveal-inner {
  display: block;
  transform: translateY(100%);
  opacity: 0;
  transition:
    transform var(--duration-line) var(--ease-entrance),
    opacity   var(--duration-line) var(--ease-entrance);
  will-change: transform, opacity;
}
.line-reveal-inner.revealed { transform: translateY(0); opacity: 1; }

/* M1c — word stagger. For a paragraph that must be *read*, not skimmed.
   Stagger is applied in JS at var(--stagger-word) intervals. */
.word-reveal-span {
  display: inline-block;
  transform: translateY(24px);
  opacity: 0;
  transition:
    transform var(--duration-reveal) var(--ease-word),
    opacity   var(--duration-reveal) var(--ease-word);
}
.word-reveal-span.revealed { transform: translateY(0); opacity: 1; }


/* ==========================================================================
   M2 — AMBIENT
   Motion that never stops and never asks for attention. It exists so the
   page is alive before the user does anything.
   ========================================================================== */

/* M2a — gradient drift.
   Ported from textura.agency @keyframes gradientAnimation (5s ease infinite).
   Requires a background sized 200% 200% so there is travel room. */
@keyframes vp-gradient-drift {
  0%   { background-position: 0% 100%; }
  33%  { background-position: 100% 100%; }
  66%  { background-position: 100% 0%; }
  100% { background-position: 0% 100%; }
}
.gradient-drift {
  background: var(--gradient-loop) 0% 0% / 200% 200%;
  animation: vp-gradient-drift var(--loop-gradient) ease 0s infinite normal none running;
}

/* M2b — shimmer sweep.
   Ported from textura.agency `.ljefgT::after` + @keyframes shimmer.
   Used for skeleton/loading state and for the hairline under a live badge. */
@keyframes vp-shimmer {
  100% { transform: translateX(100%); }
}
.shimmer {
  position: absolute; inset: 0;
  background-color: var(--accent-05);
  overflow: hidden;
}
.shimmer::after {
  content: "";
  position: absolute; inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    rgba(83, 104, 120, 0) 0%,
    rgba(83, 104, 120, 0.10) 20%,
    rgba(83, 104, 120, 0.20) 60%,
    rgba(83, 104, 120, 0) 100%
  );
}
.shimmer.-animate::after {
  animation: vp-shimmer var(--loop-shimmer) ease 0s infinite normal none running;
}

/* M2c — slow spin.
   Ported from textura.agency @keyframes starAnimation (rotate 0 -> 360deg).
   Applied to the asterisk that sits behind a primary CTA on hover. */
@keyframes vp-star-spin {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
.star-spin { animation: vp-star-spin var(--loop-star) linear 0s infinite normal none running; }

/* M2d — pulse dot. The one ambient loop authored here rather than ported:
   the live-status dot has no equivalent in either reference. */
@keyframes vp-pulse {
  0%, 100% { opacity: 1;    box-shadow: 0 0 0 0 var(--accent-40); }
  50%      { opacity: 0.75; box-shadow: 0 0 0 6px rgba(83, 104, 120, 0); }
}
.pulse-dot { animation: vp-pulse 2.4s var(--ease-soft) infinite; }


/* ==========================================================================
   M3 — INTERACTION
   ========================================================================== */

/* M3a — the pill hover chain (Lumora, verbatim). Container scales, badge
   translates and flips to accent. Two elements, one gesture. See
   components.css for .pill-btn / .pill-badge themselves. */

/* M3b — page dim.
   Ported from textura.agency `.lbdlSd + .fade-overlay`. Hovering the primary
   CTA darkens the entire page behind it. Costs nothing, reads as intent. */
.fade-overlay {
  position: fixed; inset: 0;
  background-color: var(--ink);
  opacity: 0;
  transition: opacity var(--duration-slow) var(--ease-soft);
  pointer-events: none;
  z-index: var(--z-dim);
}
.dim-trigger:hover ~ .fade-overlay,
.dim-trigger:focus-visible ~ .fade-overlay { opacity: 0.55; }

/* The CTA itself must outrank the dim it causes */
.dim-trigger { position: relative; z-index: calc(var(--z-dim) + 1); }

/* M3c — dual-layer text swap.
   Ported from textura.agency `.gHsQCg .link .first/.second/.hidden`, including
   the 10deg rotation that keeps it from feeling mechanical. `.hidden` is a
   zero-opacity copy that reserves the box so the two absolute layers have
   something to size against. */
.link-swap .link { position: relative; overflow: hidden; display: inline-block; }
.link-swap .hidden { opacity: 0; }
.link-swap .first,
.link-swap .second {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  transition:
    transform var(--duration-fast) var(--ease-out),
    opacity   var(--duration-fast) var(--ease-out);
}
.link-swap .first  { opacity: 1; transform: none; }
.link-swap .second { opacity: 0; transform: translateY(130%) rotate(10deg); color: var(--accent); }
.link-swap:hover .first,
.link-swap:focus-visible .first  { opacity: 0; transform: translateY(-130%) rotate(10deg); }
.link-swap:hover .second,
.link-swap:focus-visible .second { opacity: 1; transform: none; }

/* M3d — inline link nudge (Lumora `.animated-link`, verbatim) */
.animated-link {
  display: inline-flex; align-items: center;
  transition: all var(--duration-hover) var(--ease-standard);
}
@media (hover: hover) {
  .animated-link:hover { transform: translateX(4px); opacity: 1 !important; color: var(--accent); }
  .legal-link:hover    { transform: translateX(3px); opacity: 1 !important; color: var(--accent); }
}

/* M3e — the orbiting mark behind a CTA.
   Ported from textura.agency `.lbdlSd .star`: scale(0)->scale(1) with the
   transform deliberately delayed 200ms behind the opacity, so it blooms
   rather than pops.
   Two nested elements on purpose: the OUTER span owns position + scale, the
   INNER svg owns the spin. Putting both on one element loses the fight — an
   `animation` transform overrides a `transition` transform, so the mark would
   spin correctly and then jump out of its centred position. textura splits it
   the same way (`.star` positions, `.star svg` rotates). */
.cta-star {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(0);
  opacity: 0;
  width: 2.5rem; height: 2.5rem;
  pointer-events: none;
  color: var(--on-dark-40);
  transition: opacity var(--duration-fast), transform var(--duration-slow) 200ms;
}
.cta-star > svg { width: 100%; height: 100%; }
@media (hover: hover) {
  .pill-btn:hover .cta-star,
  .pill-btn:focus-visible .cta-star { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
/* Touch parity — same reasoning as .pill-btn:active in components.css */
.pill-btn:active .cta-star { opacity: 1; transform: translate(-50%, -50%) scale(1); }


/* ==========================================================================
   M4 — SCROLL
   Ported from textura.agency `.fLxUCv`: a 200vh track whose child is pinned
   at 100vh, with the negative bottom margin pulling the next section up so
   the pin costs no perceived page length. GSAP ScrollTrigger drives the
   scrubbed values; the pin itself is pure CSS and survives GSAP failing to
   load.
   ========================================================================== */
.sticky-scene {
  position: relative; top: 0; left: 0;
  width: 100%;
  height: calc(var(--vh, 1vh) * 200);
  margin-bottom: calc(var(--vh, 1vh) * -100);
}
.sticky-scene .sticky {
  position: sticky; top: 0; left: 0;
  width: 100%;
  height: calc(var(--vh, 1vh) * 100);
  overflow: hidden;
}

/* Curtain wipes that ride the pin, driven by ScrollTrigger */
.border-top,
.border-bottom {
  position: absolute; left: 0;
  width: 100%; height: 12vh;
  z-index: var(--z-content);
  pointer-events: none;
  background: var(--background);
}
.border-top    { top: 0;    transform: translateY(-100%); }
.border-bottom { bottom: 0; transform: translateY(100%); }

/* Difference blend — textura's hero signature: white type over a moving
   backdrop reads as a hole punched through it.

   HARD CONSTRAINT, measured, do not remove:
   difference renders white text as |255 - backdrop|. That is only legible
   when the backdrop is near-black (text -> near-white) or near-white (text ->
   near-black). Against a MID-TONE backdrop the two converge and contrast
   collapses to ~1:1 — the text disappears. textura gets away with it because
   its hero backdrop is a dark video. This system's hero field is a light
   mid-tone ramp (#ecebe9 -> #a3b1bc, darkened further by the accent
   multiply to ~rgb(130)), where difference output lands at ~rgb(125) — a
   1.03:1 contrast ratio.

   THEREFORE: `.blend-difference` is approved for DECORATIVE elements only
   (rules, geometric marks, watermarks). It is forbidden on text in this
   system. The hero headline uses solid `--foreground` instead: 17.99:1.
   See docs/foundations/color.md and ADR-0006. */
.blend-difference { mix-blend-mode: difference; color: #fff; }


/* ==========================================================================
   MOBILE ENTRANCE — same M1 timings as desktop, verbatim.
   --------------------------------------------------------------------------
   Used to be killed below 1024px because momentum scroll after a flick can
   catch content mid-transition. Kept instead by lowering the
   IntersectionObserver threshold on touch (vp-motion.js), which fires the
   reveal earlier — before the element is likely to still be moving when the
   finger lifts — rather than by removing the animation.
   ========================================================================== */
