/* ============================================================
   Keyframes & scroll-reveal placeholder
   Per-component transitions live alongside their components in
   components.css; only globally-named animations are defined here.
   ============================================================ */

/* status LED in the brand mark */
@keyframes led-pulse { 50% { opacity: 0.5; } }

/* horizontal kinetic strip / marquee */
@keyframes marq {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* ============ scroll reveal — entrance via framer-motion ============
   The actual tween is run by framer-motion (see useReveal() in
   primitives.jsx). These rules now only set the "from" state so each
   .reveal element doesn't briefly render at its rest position between
   mount and the first FM frame. FM disables the transition inline
   before it animates, so the transition properties below are inert
   once FM has taken over — they only matter as a fallback when FM
   has not yet loaded.

   Variants (apply alongside `.reveal`):
     .reveal-fade   — fade only, no displacement
     .reveal-scale  — fade + slight scale
     .reveal-blur   — fade + clear blur
     .reveal-stagger — children stagger via :nth-child delays (fallback) */

.reveal {
  opacity: 0;
  transform: translateY(24px);
  will-change: opacity, transform;
}
/* `.in` is still added by useReveal() so descendants reading .in
   (e.g. scroll-link strip parents) keep working — but FM controls the
   tween, so this rule only matters in the FM-fails-to-load fallback. */
.reveal.in {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
  transition:
    opacity .9s cubic-bezier(.22, .8, .2, 1),
    transform .9s cubic-bezier(.22, .8, .2, 1),
    filter .9s cubic-bezier(.22, .8, .2, 1);
}

/* — variants — */
.reveal.reveal-fade        { transform: none; }
.reveal.reveal-fade.in     { transform: none; }

.reveal.reveal-scale       { transform: translateY(16px) scale(0.96); }
.reveal.reveal-scale.in    { transform: translateY(0) scale(1); }

.reveal.reveal-blur        { filter: blur(10px); }
.reveal.reveal-blur.in     { filter: blur(0); }

/* Stagger fallback: only kicks in if FM never loaded — in that case
   useReveal() adds .in to the parent and the :nth-child rules below
   animate children in sequence. With FM, per-tile reveal handles the
   stagger and parent .reveal-stagger is stripped early. */
.reveal-stagger > * {
  opacity: 0;
  transform: translateY(18px);
}
.reveal-stagger.in > * {
  transition:
    opacity .8s cubic-bezier(.22, .8, .2, 1),
    transform .8s cubic-bezier(.22, .8, .2, 1);
}
.reveal-stagger.in > *:nth-child(1)  { opacity: 1; transform: none; transition-delay: 40ms; }
.reveal-stagger.in > *:nth-child(2)  { opacity: 1; transform: none; transition-delay: 90ms; }
.reveal-stagger.in > *:nth-child(3)  { opacity: 1; transform: none; transition-delay: 140ms; }
.reveal-stagger.in > *:nth-child(4)  { opacity: 1; transform: none; transition-delay: 190ms; }
.reveal-stagger.in > *:nth-child(5)  { opacity: 1; transform: none; transition-delay: 240ms; }
.reveal-stagger.in > *:nth-child(6)  { opacity: 1; transform: none; transition-delay: 290ms; }
.reveal-stagger.in > *:nth-child(7)  { opacity: 1; transform: none; transition-delay: 340ms; }
.reveal-stagger.in > *:nth-child(8)  { opacity: 1; transform: none; transition-delay: 390ms; }
.reveal-stagger.in > *:nth-child(n+9) { opacity: 1; transform: none; transition-delay: 440ms; }

@media (prefers-reduced-motion: reduce) {
  .reveal,
  .reveal-stagger > * {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }
}

/* ============ hero — staggered entrance after the splash ============
   The loading splash covers everything for the first 1400ms of every
   hard reload. The inline script in index.html then adds `.intro-end`
   to <body>, triggering this cascade of CSS @keyframes — one piece
   appears at a time. The keyframes only RUN when `.intro-end` is on
   <body>, so prior to the splash fading there is nothing visible to
   double-play even though React's createRoot rebuilt the DOM.
   In-app SPA navigation keeps `.intro-end` on <body>, so re-mounts
   immediately resolve to final state via `forwards`, no re-cascade.
   The lead headline keeps its own JS-driven particle reveal.
   Solid dark hero background so any momentary canvas-not-yet-rendered
   gap reads as intentional theme color rather than page bg. */
.theme-dark.hero-shader-section,
section.hero-shader-section.theme-dark {
  background: #080808;
}

@keyframes hero-canvas-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes hero-fade-up {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Default (pre-.intro-end): hero pieces are HIDDEN. `!important` is
   required because the cursor-proximity mood rules in components.css
   (e.g. [data-hero-mood="spotlight"] .hero-shader-line--sub) set
   opacity at the same specificity AND load later, so they'd win in
   a normal cascade. Once <body>.intro-end is added, the keyframe rule
   below also uses !important to take over and animate to opacity 1. */
body:not(.intro-end) .hero-shader-section .hero-canvas,
body:not(.intro-end) .hero-shader-section .hero-eyebrow,
body:not(.intro-end) .hero-shader-section .hero-shader-line--lead,
body:not(.intro-end) .hero-shader-section .hero-shader-line--sub,
body:not(.intro-end) .hero-shader-section .hero-shader-ctas {
  opacity: 0 !important;
}
body:not(.intro-end) .hero-shader-section .hero-eyebrow,
body:not(.intro-end) .hero-shader-section .hero-shader-line--sub,
body:not(.intro-end) .hero-shader-section .hero-shader-ctas {
  transform: translateY(14px) !important;
}

/* Cascade — fires when the inline script flips <body>.intro-end on at
   the moment the splash begins fading. Each piece has an incremental
   delay so they appear "one by one" through the dissolving splash.
   `both` fill-mode means the FROM state (opacity 0) is applied DURING
   the delay too — without it, the staggered elements would flash to
   their resting opacity during the wait and then animate from 0. */
body.intro-end .hero-shader-section .hero-canvas {
  animation: hero-canvas-in 700ms cubic-bezier(.22, .8, .2, 1) 0ms both;
}
body.intro-end .hero-shader-section .hero-eyebrow {
  animation: hero-fade-up 600ms cubic-bezier(.22, .8, .2, 1) 250ms both;
}
body.intro-end .hero-shader-section .hero-shader-line--sub {
  animation: hero-fade-up 600ms cubic-bezier(.22, .8, .2, 1) 850ms both;
}
body.intro-end .hero-shader-section .hero-shader-ctas {
  animation: hero-fade-up 600ms cubic-bezier(.22, .8, .2, 1) 1150ms both;
}

/* Reduced motion: skip the cascade — snap to final state. */
@media (prefers-reduced-motion: reduce) {
  body:not(.intro-end) .hero-shader-section .hero-canvas,
  body:not(.intro-end) .hero-shader-section .hero-eyebrow,
  body:not(.intro-end) .hero-shader-section .hero-shader-line--lead,
  body:not(.intro-end) .hero-shader-section .hero-shader-line--sub,
  body:not(.intro-end) .hero-shader-section .hero-shader-ctas,
  body.intro-end .hero-shader-section .hero-canvas,
  body.intro-end .hero-shader-section .hero-eyebrow,
  body.intro-end .hero-shader-section .hero-shader-line--sub,
  body.intro-end .hero-shader-section .hero-shader-ctas {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }
}

/* ============ particle text reveal ============
   Disabled at the JS layer — previous implementation hid chars at
   opacity 0 before the animation could fire, leaving headings
   invisible. CSS rule defaults chars to fully visible so even if
   the splitter accidentally runs, text remains readable. */
.reveal-char {
  display: inline;
  opacity: 1;
}

/* ============ section-hero — staggered entrance after the splash ============
   Same cascade pattern as the homepage hero. Pre-.intro-end the eyebrow,
   title, and subtitle are invisible. When the inline script flips
   <body>.intro-end on (at the moment the splash begins fading), each
   piece fades up one at a time. */
body:not(.intro-end) .page-hero-eye,
body:not(.intro-end) .page-hero-title,
body:not(.intro-end) .page-hero-sub {
  opacity: 0 !important;
  transform: translateY(14px) !important;
}

body.intro-end .page-hero-eye {
  animation: hero-fade-up 550ms cubic-bezier(.22, .8, .2, 1) 0ms both;
}
body.intro-end .page-hero-title {
  animation: hero-fade-up 800ms cubic-bezier(.22, .8, .2, 1) 300ms both;
}
body.intro-end .page-hero-sub {
  animation: hero-fade-up 600ms cubic-bezier(.22, .8, .2, 1) 800ms both;
}

@media (prefers-reduced-motion: reduce) {
  body:not(.intro-end) .page-hero-eye,
  body:not(.intro-end) .page-hero-title,
  body:not(.intro-end) .page-hero-sub,
  body.intro-end .page-hero-eye,
  body.intro-end .page-hero-title,
  body.intro-end .page-hero-sub {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }
}

/* ============ tile reveal — per-card scroll animation ============
   Driven by scroll-reveal-tiles.js. Each tile gets `.tile-reveal` plus
   an `--i` index for stagger. CSS custom properties on :root (set from
   the tweaks panel) control distance, scale, blur, rotation, duration,
   stagger, easing. The `data-tile-style` attribute on <html> picks the
   variant — combinations of translate/scale/blur/rotate. */
:root {
  --tile-distance: 36px;
  --tile-scale: 0.94;
  --tile-blur: 6px;
  --tile-rotate: 0deg;
  --tile-duration: 900ms;
  --tile-stagger: 70ms;
  --tile-easing: cubic-bezier(.22, .8, .2, 1);
}

/* Tile reveal is now driven by framer-motion (scroll-reveal-tiles.js).
   The CSS rule below sets the "from" state so tiles don't flash at
   rest between HTML paint and the first JS frame. FM overrides the
   transform/opacity via inline styles on each animation frame — no CSS
   transition needed (one would race with FM's per-frame writes). */
.tile-reveal {
  opacity: 0;
  transform: translateY(var(--tile-distance));
  will-change: opacity, transform, filter;
}
/* From-state variants — composed with translate so the JS-set inline
   transform overrides cleanly. */
html[data-tile-style="scale"] .tile-reveal {
  transform: translateY(var(--tile-distance)) scale(var(--tile-scale));
}
html[data-tile-style="blur"] .tile-reveal {
  transform: translateY(var(--tile-distance));
  filter: blur(var(--tile-blur));
}
html[data-tile-style="rotate"] .tile-reveal {
  transform: translateY(var(--tile-distance)) rotate(var(--tile-rotate));
}
html[data-tile-style="mixed"] .tile-reveal {
  transform: translateY(var(--tile-distance)) scale(var(--tile-scale)) rotate(var(--tile-rotate));
  filter: blur(var(--tile-blur));
}
html[data-tile-style="off"] .tile-reveal,
html[data-tile-style="off"] .tile-reveal.in {
  opacity: 1 !important;
  transform: none !important;
  filter: none !important;
  transition: none !important;
}

@media (prefers-reduced-motion: reduce) {
  .tile-reveal,
  .tile-reveal.in {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }
}

/* ============ scroll-linked tiles — driven by scroll-link-cards.js ============
   Each card receives a unitless `--p` (0..1) updated on every scroll
   frame. Opacity / translate / blur interpolate directly from `--p`
   so the cards reveal in lockstep with scroll position and reverse if
   the user scrolls back up. Translate uses the standalone `translate`
   property so it composes with the hover `transform: translateY(-1px)`
   without fighting transitions. */
.scroll-link {
  --p: 0;
  --scroll-link-distance: 28px;
  --scroll-link-blur: 3px;
  opacity: var(--p);
  translate: 0 calc((1 - var(--p)) * var(--scroll-link-distance));
  /* Subtle blur — 3px max — so cards are never illegible during scrub.
     The previous 6px blur left lower in-view cards (p≈0.7) at ~1.7px
     residual blur which read as broken. */
  filter: blur(calc((1 - var(--p)) * var(--scroll-link-blur)));
  will-change: opacity, translate, filter;
}

@media (prefers-reduced-motion: reduce) {
  .scroll-link {
    --p: 1 !important;
    translate: none !important;
    filter: none !important;
  }
}

/* ============ loading splash ============
   A full-viewport dark splash that covers every hard-reloaded page
   until React is mounted AND the minimum splash duration has passed.
   The fade-out is gated on <body>.intro-end (set by app.js useEffect)
   so the splash stays visible until React is ready to drive the hero
   cascade — no blank gap.
   The splash is forced dark regardless of the page's light/dark theme,
   so colors are literals rather than --fg/--bg tokens. */
.page-intro {
  position: fixed;
  inset: 0;
  z-index: 10000;
  background: #050505;
  color: #f5f5f2;
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: opacity;
  opacity: 1;
}
body.intro-end .page-intro {
  animation: page-intro-out 500ms cubic-bezier(.22, .8, .2, 1) 0ms forwards;
}
.page-intro-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  padding: 0 24px;
}
.page-intro-mark {
  font-family: var(--font-sans, 'Strawford', sans-serif);
  font-weight: 450;
  font-size: clamp(28px, 4vw, 56px);
  letter-spacing: -0.03em;
  color: #f5f5f2;
  opacity: 0;
  transform: translateY(8px);
  animation: page-intro-mark-in 550ms cubic-bezier(.22, .8, .2, 1) 80ms forwards;
}
.page-intro-bar {
  width: clamp(180px, 26vw, 360px);
  height: 1px;
  background: rgba(245, 245, 242, 0.10);
  position: relative;
  overflow: hidden;
}
.page-intro-bar-fill {
  position: absolute;
  inset: 0;
  background: var(--hot, #ff4a1c);
  transform-origin: left center;
  transform: scaleX(0);
  /* The bar fill IS the loading affordance — when it reaches the right
     edge, the splash starts fading. The duration matches the splash's
     fade-out trigger (1400ms) minus the 200ms head start. */
  animation: page-intro-bar 1200ms cubic-bezier(.4, 0, .2, 1) 200ms forwards;
  box-shadow: 0 0 12px color-mix(in srgb, var(--hot, #ff4a1c) 60%, transparent);
}
.page-intro-status {
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(245, 245, 242, 0.55);
  opacity: 0;
  animation: page-intro-mark-in 550ms cubic-bezier(.22, .8, .2, 1) 220ms forwards;
}

@keyframes page-intro-mark-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes page-intro-bar {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
@keyframes page-intro-out {
  to {
    opacity: 0;
    pointer-events: none;
    visibility: hidden;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Skip the splash entirely under reduced motion — content shows
     immediately. The React component still mounts briefly but the CSS
     animation jumps straight to the end state. */
  .page-intro {
    animation: none;
    opacity: 0;
    pointer-events: none;
    visibility: hidden;
  }
}
