/* Built & Run motion primitives
   Self-contained, no dependencies. Pair with motion.js (only 6 of the 12 need it).

   Three rules this file never breaks:
   1. Only transform and opacity are animated. Anything else triggers layout or paint.
   2. Under prefers-reduced-motion: reduce, every effect below is inert and content is visible.
   3. Nothing is hidden unless JS confirmed it can un-hide it (the .motion-ready gate). */

:root {
  --motion-base: 0.7s;
  --motion-fast: 0.25s;
  --motion-ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* CSS equivalent of GSAP's expo.out: moves fast, settles slowly. This single
     curve is most of what makes an expensive site feel expensive. */
  --motion-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --motion-reveal: 0.9s;
  --motion-draw: 1.1s;
  --motion-rise: 26px;
  --motion-stagger: 70ms;
}

/* Shared end state. Both the CSS-driven and JS-driven paths animate to this. */
@keyframes m-reveal {
  from { opacity: 0; transform: translate3d(var(--m-x, 0), var(--m-y, var(--motion-rise)), 0) scale(var(--m-s, 1)); }
  to   { opacity: 1; transform: none; }
}

@keyframes m-parallax {
  from { transform: translate3d(0, var(--parallax, 60px), 0); }
  to   { transform: translate3d(0, calc(var(--parallax, 60px) * -1), 0); }
}

/* Counter-parallax: explicit start and end in percent, so two elements can travel
   in opposite directions and slide past each other. Symmetric drift reads as lag;
   opposing travel reads as deliberate. Percent, not pixels, so it scales with the
   element rather than needing per-breakpoint tuning. */
@keyframes m-parallax-ft {
  from { transform: translate3d(0, var(--parallax-from, 20%), 0); }
  to   { transform: translate3d(0, var(--parallax-to, -20%), 0); }
}

@keyframes m-line {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

@keyframes m-marquee {
  to { transform: translate3d(calc(-100% - var(--marquee-gap, 3rem)), 0, 0); }
}

@keyframes m-mesh {
  from { transform: translate3d(-4%, -3%, 0) scale(1); }
  to   { transform: translate3d(4%, 3%, 0) scale(1.18); }
}

@keyframes m-grain {
  0%   { transform: translate3d(0, 0, 0); }
  33%  { transform: translate3d(-3%, 2%, 0); }
  66%  { transform: translate3d(2%, -3%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* Direction and flavour modifiers for reveal. */
[data-reveal="left"]  { --m-x: calc(var(--motion-rise) * -1); --m-y: 0; }
[data-reveal="right"] { --m-x: var(--motion-rise); --m-y: 0; }
[data-reveal="scale"] { --m-y: 12px; --m-s: 0.94; }

/* ---------------------------------------------------------------------------
   Everything below is opt-in on motion preference. Reduced motion gets nothing,
   which is the correct static page, not a degraded one.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {

  /* --- 1. Scroll reveal + 2. Parallax ---
     Two paths. motion.js picks between them and sets the class, because CSS.supports()
     reporting animation-timeline is not proof the timeline actually runs: it can parse
     and still sit permanently inactive (currentTime null), which silently no-ops every
     scroll animation on the page. The script probes a real ViewTimeline and only adds
     .motion-css once it has seen a live currentTime. */

  /* Path A: native scroll-driven, zero JS per frame. */
  @supports (animation-timeline: view()) {
    .motion-css [data-reveal] {
      animation: m-reveal linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 55%;
    }
    .motion-css [data-parallax] {
      animation: m-parallax linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
      will-change: transform;
    }
    .motion-css [data-parallax-from] {
      animation: m-parallax-ft linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
      will-change: transform;
    }
  }

  /* Path B: IntersectionObserver + shared rAF. .motion-ready is only ever set by
     motion.js, so if the script never runs, nothing is hidden. */
  .motion-ready:not(.motion-css) [data-reveal] {
    opacity: 0;
    transform: translate3d(var(--m-x, 0), var(--m-y, var(--motion-rise)), 0) scale(var(--m-s, 1));
    transition: opacity var(--motion-base) var(--motion-ease),
                transform var(--motion-base) var(--motion-ease);
    transition-delay: var(--m-delay, 0ms);
  }
  .motion-ready:not(.motion-css) [data-reveal].is-in { opacity: 1; transform: none; }
  .motion-ready:not(.motion-css) [data-parallax],
  .motion-ready:not(.motion-css) [data-parallax-from] { will-change: transform; }

  /* --- 4. Split text: masked reveal, JS wraps the spans ---
     Each part sits inside an overflow-hidden mask and starts fully below its own
     clipping edge, so words rise out from behind the line rather than fading in
     place. The mask is what separates this from the cheap version, and it is why
     there is no opacity fade: with a hard edge a fade just looks muddy. */
  .motion-ready [data-split] .m-char {
    display: inline-block;
    transform: translate3d(0, 115%, 0);
    transition: transform var(--motion-reveal, 0.9s) var(--motion-expo);
    transition-delay: calc(var(--m-i, 0) * var(--motion-stagger));
  }
  .motion-ready [data-split].is-in .m-char { transform: none; }

  /* --- 13. Staged page-load entrance --- */
  .motion-ready [data-anim-load] {
    opacity: 0;
    transform: translate3d(0, var(--motion-rise), 0);
    transition: opacity var(--motion-base) var(--motion-expo),
                transform var(--motion-base) var(--motion-expo);
    transition-delay: var(--m-load-delay, 0s);
  }
  .motion-ready [data-anim-load="fade"]  { transform: none; }
  .motion-ready [data-anim-load="scale"] { transform: scale(0.96); }
  .motion-ready [data-anim-load="mask"] .m-char {
    transform: translate3d(0, 115%, 0);
    transition: transform var(--motion-reveal, 0.9s) var(--motion-expo);
    transition-delay: calc(var(--m-load-delay, 0s) + var(--m-i, 0) * var(--motion-stagger));
  }
  .motion-ready [data-anim-load="mask"] { opacity: 1; transform: none; }
  .motion-ready.is-loaded [data-anim-load] { opacity: 1; transform: none; }
  .motion-ready.is-loaded [data-anim-load="mask"] .m-char { transform: none; }

  /* --- 15. Line draw --- */
  .motion-css [data-line] {
    transform-origin: left center;
    animation: m-line linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 45%;
  }
  .motion-ready:not(.motion-css) [data-line] {
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--motion-draw, 1.1s) var(--motion-expo);
  }
  .motion-ready:not(.motion-css) [data-line].is-in { transform: none; }

  /* --- 8. Infinite marquee: pure CSS, needs two identical track children --- */
  .marquee > * { animation: m-marquee var(--marquee-speed, 30s) linear infinite; }
  .marquee:hover > * { animation-play-state: paused; }

  /* --- 9. Gradient mesh: pure CSS --- */
  .mesh::before { animation: m-mesh var(--mesh-speed, 22s) ease-in-out infinite alternate; }

  /* --- 10. Film grain: pure CSS --- */
  .grain::after { animation: m-grain 0.7s steps(2) infinite; }
}

/* ---------------------------------------------------------------------------
   Static structure. These render correctly with no motion and no JS.
   --------------------------------------------------------------------------- */

/* 4 and 13. Mask for text reveals.
   The padding/negative-margin pair stops descenders (g, y, p) being clipped by the
   overflow edge. That one detail is the difference between a masked reveal that
   looks expensive and one that looks broken. */
.m-mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.18em;
  margin-bottom: -0.18em;
}

/* The untouched original, kept for assistive tech and clipped out of view. Absolute
   so it costs no layout. Never display:none: that would hide it from screen readers
   too, which is the whole point of keeping it. */
.m-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* 16. Scroll assembly scene.
   A tall scroller with a pinned stage inside it. motion.js publishes --build (0 to 1)
   and data-stage on the scene; the page keys its own reveals off [data-stage="n"]. */
.build-scene { position: relative; }
.build-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: clip;
  display: grid;
  place-items: center;
}
@media (prefers-reduced-motion: reduce) {
  .build-pin { position: static; height: auto; overflow: visible; }
}

/* 8. Marquee */
.marquee {
  display: flex;
  gap: var(--marquee-gap, 3rem);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.marquee > * {
  display: flex;
  flex: 0 0 auto;
  gap: var(--marquee-gap, 3rem);
  min-width: 100%;
  align-items: center;
}

/* 9. Gradient mesh */
.mesh { position: relative; isolation: isolate; overflow: hidden; }
.mesh::before {
  content: "";
  position: absolute;
  inset: -30%;
  z-index: -1;
  opacity: var(--mesh-opacity, 0.5);
  filter: blur(48px);
  background:
    radial-gradient(38% 38% at 20% 30%, var(--mesh-1, #19b5c9) 0%, transparent 62%),
    radial-gradient(34% 34% at 80% 22%, var(--mesh-2, #e8501a) 0%, transparent 62%),
    radial-gradient(44% 44% at 62% 80%, var(--mesh-3, #7c5cff) 0%, transparent 62%);
}

/* 10. Film grain */
.grain { position: relative; }
.grain::after {
  content: "";
  position: absolute;
  inset: -12%;
  z-index: 3;
  pointer-events: none;
  opacity: var(--grain-opacity, 0.16);
  mix-blend-mode: overlay;
  background-size: 170px 170px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* 5. Magnetic button (JS sets --m-mx/--m-my) */
[data-magnetic] {
  transform: translate3d(var(--m-mx, 0px), var(--m-my, 0px), 0);
  transition: transform var(--motion-fast) var(--motion-ease);
}
[data-magnetic].is-pulled { transition: none; }

/* 6. Custom cursor */
.m-cursor {
  position: fixed;
  top: 0; left: 0;
  z-index: 9999;
  width: var(--cursor-size, 26px);
  height: var(--cursor-size, 26px);
  margin: calc(var(--cursor-size, 26px) / -2) 0 0 calc(var(--cursor-size, 26px) / -2);
  border: 1.5px solid var(--cursor-color, currentColor);
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s, width 0.25s var(--motion-ease), height 0.25s var(--motion-ease),
              margin 0.25s var(--motion-ease), background 0.25s;
  will-change: transform;
}
.m-cursor.is-visible { opacity: 1; }
.m-cursor.is-hover {
  --cursor-size: 54px;
  background: var(--cursor-hover-fill, rgba(232, 80, 26, 0.16));
}

/* 7. Card tilt (JS sets --m-rx/--m-ry) */
[data-tilt] {
  transform: perspective(var(--tilt-depth, 900px))
             rotateX(var(--m-rx, 0deg)) rotateY(var(--m-ry, 0deg));
  transform-style: preserve-3d;
  transition: transform var(--motion-fast) var(--motion-ease);
}
[data-tilt].is-tilting { transition: none; }

/* 11. Count-up + 12. Text scramble: no layout shift while the value changes */
[data-countup], [data-scramble] { font-variant-numeric: tabular-nums; }

/* 3. Sticky scroll sequence */
.sticky-seq { position: relative; }
.sticky-seq__pin {
  position: sticky;
  top: var(--seq-top, 0);
  height: var(--seq-height, 100vh);
  display: grid;
  place-items: center;
  overflow: hidden;
}
.sticky-seq__step {
  grid-area: 1 / 1;
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition: opacity 0.45s var(--motion-ease), transform 0.45s var(--motion-ease);
}
.sticky-seq__step.is-active { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .sticky-seq__pin { position: static; height: auto; display: block; }
  .sticky-seq__step { opacity: 1; transform: none; }
}
