@charset "UTF-8";
/* =====================================================================
   MOTION — one-time first-view reveal, and the tokens every other
   stylesheet borrows for its own transitions.

   Phase 4.1 centralised this and Phase 5 trimmed it. Before, durations and
   easings were written inline in five files, so "make it smoother" meant
   hunting for numbers. Now there is one ease and three durations, and every
   other stylesheet references them instead of writing a literal.

   The shell and menu durations were removed with the application shell — there
   is no rail to slide and no drawer to open.

   Progressive enhancement: every hidden state is scoped under the `.js` class
   that main.js adds to <html> at startup, so with JavaScript unavailable (or
   if init throws) all content is simply visible. Elements are unobserved after
   their first reveal and never replay.
   ===================================================================== */

:root {
  /* One ease for everything that enters: a long, decelerating settle with no
     overshoot. Steeper curves read as a bounce on a page this dense. */
  --motion-ease-out: cubic-bezier(.22, 1, .36, 1);
  /* Kept as an alias so any rule written against the Phase 5.1 name resolves
     to the same curve — there is still exactly one easing on the page. */
  --motion-ease: var(--motion-ease-out);

  /* Three durations, chosen by how much area the element covers. Bigger things
     take longer, which is what makes a mixed page feel coherent. All sit in the
     650–900ms band the Phase 5 motion direction asks for. */
  --motion-dur-section: 880ms;   /* section headings / full-width bands */
  --motion-dur-card:    720ms;   /* reason cards, machines, steps, trust rows */
  --motion-dur-small:   520ms;   /* eyebrow, stat badge, mark strips */

  /* Gap between consecutive items in one group. main.js drops this to 50ms
     under 768px, where a long chain reads as lag rather than rhythm. */
  --motion-stagger: 80ms;
}

/* ---------------------------------------------------------------------
   First-view reveal
   --------------------------------------------------------------------- */
.js [data-reveal] {
  opacity: 0;
  /* transform + opacity only — never `all`, never filters or backgrounds. */
  transition-property: opacity, transform;
  transition-timing-function: var(--motion-ease-out);
  transition-duration: var(--motion-dur-card);
}
/* No permanent will-change: main.js adds it just before the reveal and removes
   it when the transition is done. */
.js [data-reveal="rise"] {
  transform: translateY(22px);
  transition-duration: var(--motion-dur-section);
}
.js [data-reveal="card"] {
  transform: translateY(20px);
  transition-duration: var(--motion-dur-card);
}
.js [data-reveal="pop"] {
  transform: translateY(10px) scale(.92);
  transition-duration: var(--motion-dur-small);
}
.js [data-reveal="machine"] {
  transform: rotate(var(--tilt, 0deg)) translateY(26px) scale(.96);
  transition-duration: var(--motion-dur-card);
}
/* The H1 has no reveal hook at all: it must never be hidden or moved
   while the browser measures Largest Contentful Paint. */

.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}
/* Machines keep their tilt after revealing. */
.js [data-reveal="machine"].is-revealed { transform: rotate(var(--tilt, 0deg)); }

/* ---------------------------------------------------------------------
   Choreography: heading -> cards -> supporting

   `--reveal-lead` is the head start a group gives the element above it. A
   card grid sets it to one stagger unit so its heading always lands first,
   even when heading and grid cross the threshold in the same frame.
   `--card-index` / `--reveal-order` come from the templates.
   --------------------------------------------------------------------- */
.js [data-reveal].is-revealed { transition-delay: var(--reveal-lead, 0ms); }
.js [data-reveal="card"].is-revealed {
  transition-delay: calc(var(--reveal-lead, 0ms) + var(--card-index, 0) * var(--motion-stagger));
}
.js [data-reveal="pop"].is-revealed {
  transition-delay: calc(var(--reveal-lead, 0ms) + var(--card-index, 0) * var(--motion-stagger) * .7);
}
/* Hero fan: rear-left, then rear-right, then the dominant centre cabinet.
   The order is data (heroMachines.json -> revealOrder), not a selector. */
.js [data-reveal="machine"].is-revealed {
  transition-delay: calc(var(--reveal-order, 1) * var(--motion-stagger));
}
/* Registered-player badge (Phase 5.7): the stamp carries a permanent slight
   rotation (tokens.css --badge-rotate) as its identity, so the reveal pop
   must layer on top of that rotation rather than replace it with `none` -
   same pattern as the hero machine fan's --tilt above. */
.js [data-reveal="pop"].stat-badge {
  transform: rotate(var(--badge-rotate)) translateY(10px) scale(.92);
}
.js [data-reveal="pop"].stat-badge.is-revealed {
  transform: rotate(var(--badge-rotate));
}

/* Every grid that follows its own heading. One declaration, not one per
   section, so the choreography cannot drift between sections. */
.features__grid,
.lineup__grid,
.benefits__grid,
.trust__points,
.steps { --reveal-lead: var(--motion-stagger); }

/* ---------------------------------------------------------------------
   CTA ANIMATION — Phase 5.2, unified in Phase 5.4

   Every button gets the quieter sheen (`.btn--primary::after`). All four
   registration CTAs (header, hero, lineup, final) additionally carry
   `.btn--pulse`, applied by components/cta.njk whenever `ctaKind ==
   "register"` — never per-location CSS — so the four share one identical
   pulse: same animation name, duration, timing function and transform range.
   A non-register CTA (`ctaKind` left at its "secondary" default) would keep
   the sheen only, but none exists on the page today.

   Phase 5.8L removed the `.is-onscreen` gate entirely: the pulse now runs from
   the document timeline so all five registration CTAs share one phase (see the
   block below). Only transform and opacity are animated — never width, height
   or margin — so a pulsing button can never shift the layout around it.
   --------------------------------------------------------------------- */
.btn--primary { position: relative; overflow: hidden; }

/* The moving sheen, shared by both intensities. */
.btn--primary::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(100deg, transparent 42%, rgba(255, 255, 255, .40) 50%, transparent 58%);
  transform: translateX(-130%);
  opacity: 0;
}
.js .btn--primary::after { animation: cta-sheen 6.5s var(--motion-ease-out) infinite; }
@keyframes cta-sheen {
  0%   { transform: translateX(-130%); opacity: 0; }
  4%   { opacity: .85; }
  16%  { transform: translateX(130%);  opacity: 0; }
  100% { transform: translateX(130%);  opacity: 0; }
}

/* Loud variant: hero + final (Phase 5.3 — noticeably more energetic than the
   quieter header/lineup sheen-only treatment). A breathing scale + lift plus
   an expanding glow ring, both on a ~1.6s cycle. The ring is a ::before on a
   wrapper-free button, so it cannot affect layout. */
.btn--pulse::before {
  content: "";
  position: absolute;
  inset: -3px;
  z-index: -1;
  border-radius: inherit;
  background: radial-gradient(closest-side, rgba(255, 227, 61, .65), rgba(236, 43, 145, .42) 70%, transparent);
  opacity: 0;
}
/* PHASE 5.8L — ONE GLOBAL PULSE PHASE, from the document timeline.
   ---------------------------------------------------------------------
   Until 5.8K the pulse was gated on `.is-onscreen` and phase-corrected with a
   negative `--pulse-delay` that main.js computed against a shared epoch the
   first time each button scrolled into view. Two things were wrong with it:

     · the JS epoch used PULSE_MS = 1600 while this animation is 1.8s, so the
       modulo produced the WRONG offset and the five buttons drifted apart by
       up to 200ms per cycle rather than locking together;
     · the phase was (re)established on an IntersectionObserver edge, so
       scrolling a CTA out and back in restarted it — exactly what a
       page-wide rhythm must not do.

   All five registration CTAs exist in the DOM at first render, so a plain CSS
   animation with NO delay and NO gating class is inherently phase-locked:
   every one of them is timed from the same document timeline origin, so at any
   instant `getAnimations()[0].currentTime` is identical across all five. There
   is no epoch to get wrong, no observer edge to restart on, and no JavaScript
   clock at all. Scroll, hover, focus, resize and returning from a hidden tab
   cannot change the phase because nothing re-declares the animation.

   Offscreen work is not a concern: browsers already throttle compositing for
   offscreen elements and suspend animation frames in a hidden tab, and these
   are five small transform/opacity animations.

   `--pulse-scale`/`--pulse-glow-scale` (Phase 5.6) let the smaller header CTA
   (`.btn--sm`, below) breathe a touch less so it never collides with the
   header bar. Only the AMPLITUDE differs — name, duration, timing function and
   delay are identical across all five, so the phase lock is unaffected. */
.js .btn--pulse {
  animation: cta-breathe 1.8s var(--motion-ease-out) infinite;
  animation-delay: 0s;
}
.js .btn--pulse::before {
  animation: cta-glow 1.8s var(--motion-ease-out) infinite;
  animation-delay: 0s;
}
/* One sheen pass per breathe cycle on the registration CTAs — the generic 6.5s
   sheen above is deliberately slower and milder for any non-register button. */
.js .btn--pulse::after {
  animation: cta-sheen 1.8s var(--motion-ease-out) infinite;
  animation-delay: 0s;
}
/* Header CTA: a slightly lower amplitude than the three full-size in-page
   CTAs (requirement: "may use a slightly lower maximum scale to prevent
   header collision"). */
.btn--sm.btn--pulse { --pulse-scale: 1.038; --pulse-glow-scale: 1.05; }
@keyframes cta-breathe {
  0%, 100% { transform: scale(1) translateY(0); }
  48%      { transform: scale(var(--pulse-scale, 1.045)) translateY(-2px); }
}
@keyframes cta-glow {
  0%, 100% { opacity: 0;   transform: scale(.96); }
  48%      { opacity: .92; transform: scale(var(--pulse-glow-scale, 1.07)); }
}
/* A press response that survives the breathing animation. */
.btn--primary:active { transform: scale(.985); }

/* The `.cta-frame` sheen that lived here was removed in Phase 4.1. It only ran
   on `.cta-frame.is-revealed`, and the hero CTA frame has no reveal hook — it
   sits above the fold and must never be hidden or moved while the browser
   measures Largest Contentful Paint. The rule therefore never fired; it was
   dead weight in the file that is supposed to be the single motion source. */

/* ---------------------------------------------------------------------
   Reduced motion — show everything immediately, run nothing.
   --------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal],
  .js [data-reveal].is-revealed {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
    transition-delay: 0s !important;
  }
  /* Every decorative loop stops: CTA pulse, CTA sheen, rotating rays, floating
     balls, twinkles and separator sweeps. Content and controls are untouched. */
  .js .btn--primary::after,
  .js .btn--pulse,
  .js .btn--pulse::before,
  .js .btn--pulse::after,
  .fx--burst::before,
  .fx--sparkles::after,
  .fx--balls,
  .amb,
  .amb::before,
  .amb::after {
    animation: none !important;
    opacity: var(--rm-opacity, 0) !important;
    transform: none !important;
  }
  .js .btn--pulse { opacity: 1 !important; }
  .fx--burst::before, .fx--sparkles::after { --rm-opacity: .35; }
  /* How-to chevrons (Phase 5.6): the small vertical bob stops, but the arrow
     itself stays fully visible — it is a static directional cue, not a
     reveal, so it must not fall into the shared 0-opacity default above. */
  .step-arrow span { animation: none !important; transform: rotate(-45deg) !important; opacity: 1 !important; }
}
