/* ==========================================================================
   FX layer — Lottie stages + the motion-graphics pass.

   Loaded after style.css, and it only ever adds. Everything here reads the
   tokens style.css already defines (--accent, --blend, --paper …), so the
   vibrant palette and the two themes come along for free. Theme-specific rules
   key off data-theme, never off prefers-color-scheme on its own: the header
   toggle can disagree with the OS, and the media query would be wrong when it
   does.
   ========================================================================== */

/* ---------- Lottie stages ---------- */
/* The container is sized here, not in the JSON: the animation is authored at a
   fixed ratio and scaled to whatever box the layout gives it. */
.fx-lottie { pointer-events: none; }
.fx-lottie svg { display: block; width: 100%; height: 100%; }

/* Held back until the player has drawn something, so an empty box never fades in
   and then jumps when the SVG lands. */
.fx-lottie { opacity: 0; transition: opacity .9s var(--ease-out); }
.fx-lottie.is-lottie { opacity: 1; }
.no-js .fx-lottie { display: none; }

/* Baked colours are mid-toned so one file reads on cream and on near-black; on the
   dark ground they want a touch more light to sit at the same weight. */
:root[data-theme="dark"] .fx-lottie { filter: brightness(1.14) saturate(1.06); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .fx-lottie { filter: brightness(1.14) saturate(1.06); }
}

/* The trend line over the hero art. Sits opposite the headline, which is capped at
   15ch, so the two never meet — and it goes away entirely before they could. */
.hero-fx {
  position: absolute;
  right: clamp(1rem, 4vw, 3.25rem);
  bottom: clamp(5rem, 15vh, 8.5rem);   /* clears the scroll cue in the corner */
  width: min(40vw, 430px);
  aspect-ratio: 16 / 9;
  z-index: 0;
  filter: drop-shadow(0 14px 34px color-mix(in srgb, var(--accent) 22%, transparent));
  /* Half the scroll rate of the headline, so the hero has depth rather than one
     flat plane sliding away. */
  transform: translate3d(0, calc(var(--sy, 0) * -0.06px), 0);
}
@media (max-width: 900px) { .hero-fx { display: none; } }

/* The signal, in the band that promises one email only when something matters. */
.subscribe-fx {
  width: min(390px, 74vw);
  aspect-ratio: 520 / 220;
  margin: 0 auto .25rem;
}

/* ---------- Hero parallax ---------- */
/* The type leads, the art trails. Both are inside .hero, which already clips, so
   nothing can be pushed out of the section. */
.hero-inner { transform: translate3d(0, calc(var(--sy, 0) * 0.16px), 0); }
.hero-media img { transform-origin: 50% 60%; }

/* ---------- Film grain ---------- */
/* style.css already lays a static grain on body::after. This one replaces it rather
   than stacking on it — two noise fields at different scales read as dirt, not film.
   Turning it off here and not there keeps fx.css purely additive: drop this file and
   the original static grain comes back on its own. */
body::after { display: none; }

/* Injected by fx.js. One layer of stepped noise over the whole site — it is what
   stops the large flat colour fields from reading as flat vector. */
.fx-grain {
  position: fixed;
  inset: -60px;              /* the step animation shifts it; this hides the seams */
  z-index: 9999;
  pointer-events: none;
  opacity: .05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: fx-grain 1.1s steps(5) infinite;
}
:root[data-theme="dark"] .fx-grain { opacity: .075; mix-blend-mode: soft-light; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .fx-grain { opacity: .075; mix-blend-mode: soft-light; }
}
@keyframes fx-grain {
  0%   { transform: translate3d(0, 0, 0); }
  20%  { transform: translate3d(-28px, 14px, 0); }
  40%  { transform: translate3d(18px, -22px, 0); }
  60%  { transform: translate3d(-14px, -18px, 0); }
  80%  { transform: translate3d(24px, 20px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* ---------- Card tilt ---------- */
/* motion.js already reports the cursor's position inside a card as --mx/--my for
   the spotlight; this is the same two numbers read as an angle, so the tilt costs
   no extra listener. Defaults to dead centre, which is no rotation at all. */
@media (hover: hover) and (min-width: 821px) {
  .grid { perspective: 1200px; }
  .card {
    transform-style: preserve-3d;
    transition: transform .5s var(--ease-out);
  }
  .card:hover {
    transform:
      rotateX(calc((var(--my, .5) - .5) * -7deg))
      rotateY(calc((var(--mx, .5) - .5) * 9deg))
      translateZ(8px);
    transition-duration: .18s;   /* follows the cursor, then settles back slowly */
  }
}

/* ---------- Magnetic buttons ---------- */
/* fx.js sets --px/--py while the cursor is over the button. The -2px lift is
   style.css's; it is restated because a transform cannot be added to, only
   replaced. */
.btn, .btn-ghost, .header-cta {
  transform: translate3d(var(--px, 0), var(--py, 0), 0);
  transition: transform .28s var(--ease-out), color .35s var(--ease), box-shadow .35s var(--ease);
}
.btn:hover, .btn-ghost:hover {
  transform: translate3d(var(--px, 0), calc(var(--py, 0px) - 2px), 0);
}

/* ---------- Ticker ---------- */
/* A light passing over the headline strip, and the faint scanline that makes it
   read as a screen rather than a border. */
.ticker { position: relative; }
.ticker::before,
.ticker::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  pointer-events: none;
}
.ticker::before {
  left: 0;
  right: 0;
  opacity: .5;
  background: repeating-linear-gradient(0deg,
    transparent 0 2px, color-mix(in srgb, var(--ink) 7%, transparent) 2px 3px);
}
.ticker::after {
  left: 0;
  width: 24%;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--accent) 30%, transparent), transparent);
  transform: translateX(-130%);
  animation: fx-sweep 7s var(--ease-out) infinite;
  animation-delay: 1.5s;
}
@keyframes fx-sweep {
  0%       { transform: translateX(-130%); }
  55%, 100% { transform: translateX(560%); }
}

/* ---------- Motion off ---------- */
/* fx.js returns before it injects the grain or publishes --sy, so these only have
   to cover what CSS starts on its own. */
@media (prefers-reduced-motion: reduce) {
  .fx-grain, .ticker::after { display: none; }
  .hero-inner, .hero-fx, .card:hover { transform: none; }
  .fx-lottie { opacity: 1; transition: none; }
}
