/* ==========================================================================
   motion.css — Apex Holding
   PURPOSE: Every animation in the system. Motion is declared in markup with
   data-attributes and driven by motion.js writing custom properties; this file
   turns those into visible behaviour. It also publishes the shared keyframe
   library the five world modules animate their artwork with.

   Loaded LAST, after chrome.css — so its reduced-motion block gets the final
   word over every other stylesheet.

   ==========================================================================
   THE TWO RULES THAT GOVERN THIS FILE

   1. TRANSFORM AND OPACITY ONLY on anything tied to scroll or pointer. No
      animated width, height, top, left, or filter on a scroll path. Every
      scroll-driven value below arrives as a custom property that feeds a
      translate3d() or a scale().

   2. NOTHING IS EVER HIDDEN OUTSIDE A .js-SCOPED RULE. Every rule that sets
      opacity:0 or clips content is prefixed with `.js`, which is added by an
      inline blocking script in <head>. With scripting off, the entire site
      renders fully visible and fully readable. This is not a nicety — it is
      the difference between a progressive enhancement and a blank page.

   ==========================================================================
   JS CONTRACT — the custom properties motion.js writes

     --stagger-index    integer, per child of a [data-reveal-group]
     --parallax-y       px offset for [data-parallax]      (clamped ±12px here)
     --tilt-x/--tilt-y  deg for [data-tilt]                (clamped ±6deg here)
     --mag-x/--mag-y    px for [data-magnetic]             (clamped ±6px here)
     --scrub-progress   0 → 1 for [data-scrub]
     --scrub-distance   px of travel for the scrub track

   The clamps are enforced in CSS as well as in JS, so a bug in one layer can
   never produce motion that exceeds the contract.

   State classes motion.js toggles:  .is-in  .is-live  .is-done

   ==========================================================================
   KEYFRAME LIBRARY — available to every world stylesheet and module.
   All are transform/opacity only unless noted.

     apex-fade-rise      entrance: 16px rise + fade in
     apex-fade-in        entrance: opacity only
     apex-float          idle: slow ±8px vertical drift, loops
     apex-shimmer        idle: highlight bar sweeping left → right
     apex-spin           idle: 360° rotation, loops
     apex-pulse-ring     idle: ring scales 1 → 1.35 and fades, loops
                         (used by the founder seal in chrome.css)
     apex-orbit          idle: 360° rotation for constellation orbit rings
     apex-orbit-reverse  idle: counter-rotation for a second orbit layer
     apex-gate-open      entrance: two gate halves part.
                         Set --gate-dir:-1 on the left half, 1 on the right.
     apex-snow-drift     idle: a snow particle falling with lateral sway.
                         Set --drift-x for its horizontal travel.
     apex-aurora         idle: slow drifting, breathing atmospheric bloom
     apex-candle-flicker idle: irregular opacity flicker (Greppo candlelight)
     apex-ember-rise     idle: an ember rising and fading (Arz Faraya hearth)
     apex-draw           entrance: SVG stroke draw-on.
                         NOT transform/opacity — animates stroke-dashoffset,
                         which is legitimate here because it is a one-shot
                         entrance and never bound to scroll. Requires
                         stroke-dasharray to be set on the path.
     apex-preload-sweep  preloader: accent bar sweeping across
     apex-preload-mark   preloader: the mark breathing while it waits

   Usage from a world stylesheet:
     .constellation__orbit { animation: apex-orbit 90s linear infinite; }
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. REVEAL — [data-reveal]
   Fade and rise, once, when the element enters the viewport.
   -------------------------------------------------------------------------- */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(16px);
  transition:
    opacity var(--dur-reveal) var(--ease-out-soft),
    transform var(--dur-reveal) var(--ease-out-soft);
}

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* Directional variants. Same timing, different origin. */
.js [data-reveal="left"]  { transform: translateX(-20px); }
.js [data-reveal="right"] { transform: translateX(20px); }
.js [data-reveal="fade"]  { transform: none; }
.js [data-reveal="scale"] { transform: scale(0.96); }


/* --------------------------------------------------------------------------
   2. REVEAL GROUP — [data-reveal-group]
   A parent that staggers its revealing children. motion.js writes an integer
   --stagger-index onto each child; the delay falls out of the multiplication.
   Override the cadence per group with --stagger-step.
   -------------------------------------------------------------------------- */

.js [data-reveal-group] [data-reveal] {
  transition-delay: calc(var(--stagger-index, 0) * var(--stagger-step, 70ms));
}


/* --------------------------------------------------------------------------
   3. PARALLAX — [data-parallax="0.15"]
   Vertical drift on scroll. The displacement is HARD CAPPED at ±12px in CSS
   regardless of what motion.js sends, so this can never turn into the kind of
   large-travel parallax that makes people ill or drops frames.
   -------------------------------------------------------------------------- */

.js [data-parallax] {
  transform: translate3d(0, clamp(-12px, var(--parallax-y, 0px), 12px), 0);
}

/* will-change costs memory for as long as it is set, so it is only present
   while the element is actually being driven. motion.js adds .is-live when
   the element enters the viewport and removes it when it leaves. */
.js [data-parallax].is-live {
  will-change: transform;
}


/* --------------------------------------------------------------------------
   4. TILT — [data-tilt]
   Pointer-tracked 3D tilt with a soft accent bloom. Capped at ±6deg. Desktop
   pointers only: a tilt that cannot be perceived without a hovering cursor is
   dead weight on touch, and worse, it can leave an element stuck mid-rotation
   after a tap.
   -------------------------------------------------------------------------- */

@media (hover: hover) and (pointer: fine) {
  .js [data-tilt] {
    transform:
      perspective(900px)
      rotateX(clamp(-6deg, var(--tilt-x, 0deg), 6deg))
      rotateY(clamp(-6deg, var(--tilt-y, 0deg), 6deg));
    transform-style: preserve-3d;
    transition:
      transform var(--dur-micro) var(--ease-out-soft),
      box-shadow var(--dur-micro) var(--ease-out-soft);
  }

  .js [data-tilt].is-live {
    will-change: transform;
    /* No transition while the pointer is driving it, or the element lags
       behind the cursor. The transition returns for the settle-back. */
    transition: box-shadow var(--dur-micro) var(--ease-out-soft);
    box-shadow: var(--shadow-3), 0 0 44px -14px var(--w-glow);
  }
}


/* --------------------------------------------------------------------------
   5. MAGNETIC — [data-magnetic]
   Element leans toward the cursor. Capped at ±6px. Fine pointers only.
   -------------------------------------------------------------------------- */

@media (hover: hover) and (pointer: fine) {
  .js [data-magnetic] {
    transform: translate3d(
      clamp(-6px, var(--mag-x, 0px), 6px),
      clamp(-6px, var(--mag-y, 0px), 6px),
      0
    );
    transition: transform var(--dur-micro) var(--ease-out-soft);
  }

  .js [data-magnetic].is-live {
    will-change: transform;
    transition: none;
  }
}


/* --------------------------------------------------------------------------
   6. COUNT UP — [data-countup]
   motion.js writes the interpolated number into the element on reveal.
   Tabular figures stop the surrounding layout from twitching as digits change.
   -------------------------------------------------------------------------- */

[data-countup] {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}


/* --------------------------------------------------------------------------
   7. SCRUB — [data-scrub]
   A staged horizontal section driven by vertical scroll. motion.js writes
   --scrub-progress (0 → 1); the track translates by that fraction of
   --scrub-distance. No transition: the value already arrives once per frame,
   and easing it would only add lag.

   MARKUP — each stage MUST carry .scrub__panel, so the touch fallback below
   can give it a snap point without reaching for a universal child selector:
     <div data-scrub style="--scrub-distance: 1200px">
       <div class="scrub__track">
         <section class="scrub__panel">…</section>
         <section class="scrub__panel">…</section>
       </div>
     </div>
   -------------------------------------------------------------------------- */

.js [data-scrub] {
  overflow: hidden;
}

.js [data-scrub] .scrub__track {
  display: flex;
  gap: var(--space-6);
  inline-size: max-content;
  transform: translate3d(
    calc(var(--scrub-progress, 0) * var(--scrub-distance, 0px) * -1),
    0,
    0
  );
}

.js [data-scrub].is-live .scrub__track {
  will-change: transform;
}

/* Touch has no reliable scrub affordance, so the track degrades to an
   ordinary swipeable rail — same content, no scroll hijacking. */
@media (hover: none) {
  .js [data-scrub] {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
  }

  .js [data-scrub] .scrub__track {
    transform: none;
  }

  .js [data-scrub] .scrub__panel {
    scroll-snap-align: start;
  }
}


/* --------------------------------------------------------------------------
   8. PRELOADER
   Covers the first paint, then fades out. Never present without script — with
   scripting off there is nothing to remove it, so it must not exist at all.
   motion.js (or core.js) adds .is-done when the page is ready.
   -------------------------------------------------------------------------- */

.preloader {
  position: fixed;
  inset: 0;
  z-index: var(--z-preloader);
  display: grid;
  place-items: center;
  gap: var(--space-6);
  background-color: var(--w-base);
  opacity: 1;
  transition: opacity var(--dur-world) var(--ease-in-out-cinema);
}

.preloader.is-done {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition:
    opacity var(--dur-world) var(--ease-in-out-cinema),
    visibility 0s linear var(--dur-world);
}

.preloader__mark {
  inline-size: 3.5rem;
  block-size: 3.5rem;
  color: var(--w-accent);
  animation: apex-preload-mark 1.6s var(--ease-in-out-cinema) infinite;
}

.preloader__bar {
  position: relative;
  inline-size: min(12rem, 40vw);
  block-size: 1px;
  overflow: hidden;
  background-color: var(--w-line);
}

.preloader__bar::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 40%;
  background-color: var(--w-accent);
  transform: translate3d(-100%, 0, 0);
  animation: apex-preload-sweep 1.2s var(--ease-in-out-cinema) infinite;
}

html:not(.js) .preloader {
  display: none;
}


/* --------------------------------------------------------------------------
   9. KEYFRAME LIBRARY
   Documented in the header block above. Worlds reference these by name.
   -------------------------------------------------------------------------- */

/* --- Entrances --------------------------------------------------------- */

@keyframes apex-fade-rise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

@keyframes apex-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes apex-gate-open {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(var(--gate-dir, 1) * 100%), 0, 0); }
}

@keyframes apex-draw {
  from { stroke-dashoffset: var(--draw-length, 1000); }
  to   { stroke-dashoffset: 0; }
}

/* --- Idle loops -------------------------------------------------------- */

@keyframes apex-float {
  0%, 100% { transform: translate3d(0, -8px, 0); }
  50%      { transform: translate3d(0, 8px, 0); }
}

@keyframes apex-shimmer {
  from { transform: translate3d(-120%, 0, 0); }
  to   { transform: translate3d(220%, 0, 0); }
}

@keyframes apex-spin {
  to { transform: rotate(360deg); }
}

@keyframes apex-pulse-ring {
  0%   { opacity: 0;   transform: scale(1); }
  15%  { opacity: 0.5; }
  70%  { opacity: 0;   transform: scale(1.35); }
  100% { opacity: 0;   transform: scale(1.35); }
}

@keyframes apex-orbit {
  to { transform: rotate(360deg); }
}

@keyframes apex-orbit-reverse {
  to { transform: rotate(-360deg); }
}

@keyframes apex-snow-drift {
  0% {
    opacity: 0;
    transform: translate3d(0, -10%, 0);
  }
  10% { opacity: 1; }
  90% { opacity: 1; }
  100% {
    opacity: 0;
    transform: translate3d(var(--drift-x, 40px), 110vh, 0);
  }
}

@keyframes apex-aurora {
  0%, 100% {
    opacity: 0.35;
    transform: translate3d(-4%, 0, 0) scale(1);
  }
  50% {
    opacity: 0.6;
    transform: translate3d(4%, -2%, 0) scale(1.12);
  }
}

@keyframes apex-candle-flicker {
  0%, 100% { opacity: 0.82; }
  12%      { opacity: 1; }
  26%      { opacity: 0.7; }
  41%      { opacity: 0.95; }
  57%      { opacity: 0.66; }
  73%      { opacity: 0.92; }
  88%      { opacity: 0.78; }
}

@keyframes apex-ember-rise {
  0% {
    opacity: 0;
    transform: translate3d(0, 0, 0) scale(0.6);
  }
  20% { opacity: 1; }
  100% {
    opacity: 0;
    transform: translate3d(var(--drift-x, 12px), -160px, 0) scale(1);
  }
}

/* --- Preloader --------------------------------------------------------- */

@keyframes apex-preload-sweep {
  0%   { transform: translate3d(-100%, 0, 0); }
  100% { transform: translate3d(250%, 0, 0); }
}

@keyframes apex-preload-mark {
  0%, 100% { opacity: 0.45; transform: scale(0.96); }
  50%      { opacity: 1;    transform: scale(1); }
}


/* ==========================================================================
   10. REDUCED MOTION — the complete override
   A visitor who has asked their operating system for less motion gets a site
   with none of it: no parallax, no tilt, no drifting particles, no orbiting,
   no scrub, no wipe, no preloader animation, no count-up crawl. Everything is
   simply, immediately there.

   This is the last block in the last stylesheet, so it wins outright without
   a single !important. motion.js must ALSO check this media query in JS and
   skip installing its rAF loops and observers entirely — CSS alone stops the
   animation, but only JS can stop the work that computes it.

   The result is not a degraded site. It is the same site, still composed,
   still considered, holding still.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {

  /* --- Reveals: already visible, nothing to wait for. ------------------ */
  .js [data-reveal],
  .js [data-reveal="left"],
  .js [data-reveal="right"],
  .js [data-reveal="fade"],
  .js [data-reveal="scale"] {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .js [data-reveal-group] [data-reveal] {
    transition-delay: 0s;
  }

  /* --- Scroll- and pointer-driven transforms: neutralised. ------------- */
  .js [data-parallax],
  .js [data-tilt],
  .js [data-magnetic] {
    transform: none;
    transition: none;
    will-change: auto;
  }

  .js [data-tilt].is-live {
    box-shadow: none;
  }

  /* --- Scrub becomes an ordinary swipeable rail. ----------------------- */
  .js [data-scrub] {
    overflow-x: auto;
    scroll-snap-type: none;
  }

  .js [data-scrub] .scrub__track {
    transform: none;
    will-change: auto;
  }

  /* --- Preloader is skipped outright, not faded. ----------------------- */
  .preloader {
    display: none;
  }

  .preloader__mark,
  .preloader__bar::after {
    animation: none;
  }

  /* --- Chrome: instant states, no glide, no pulse. --------------------- */
  .site-header,
  .site-nav__link,
  .site-nav__link::after,
  .scroll-progress__bar,
  .founder-seal,
  .founder-seal__tooltip {
    transition: none;
  }

  .founder-seal__ring {
    animation: none;
    opacity: 0;
  }

  .founder-seal:hover,
  .founder-seal:focus-visible {
    transform: none;
  }

  /* The portal wipe becomes an instant token swap: no sweep, and no delay
     before the browser navigates. transitions.js must also treat reduced
     motion as a reason to skip interception entirely. */
  .portal-wipe {
    display: none;
  }

  /* --- World switcher: tint without travel. ---------------------------- */
  .world-switcher,
  .world-switcher__wash,
  .world-switcher__tile,
  .world-switcher__tile::before,
  .world-switcher__tile-essence {
    transition: none;
  }

  .world-switcher__tile:hover,
  .world-switcher__tile:focus-visible {
    transform: none;
  }

  /* --- Components: states still change, they just do not animate. ------ */
  .btn,
  .btn::after,
  .icon-btn,
  .card,
  .chip,
  .field__input,
  .world-card,
  .world-card::before,
  .world-card__essence,
  .guide-card,
  .guide-card::after,
  .route-card,
  .accordion__trigger,
  .accordion__trigger::after,
  .accordion__panel,
  .panel,
  .scrim,
  .lightbox,
  .skip-link {
    transition: none;
  }

  /* Lift and scale removed; colour and rule changes are kept, because they
     are what actually communicates state. */
  .btn:hover,
  .btn:focus-visible,
  .btn:active,
  .icon-btn:active,
  a.card:hover,
  a.card:focus-visible,
  .world-card:hover,
  .world-card:focus-visible,
  .guide-card:hover,
  .guide-card:focus-within {
    transform: none;
  }

  /* The underline still draws — it just arrives fully formed. */
  .btn:hover::after,
  .btn:focus-visible::after {
    transform: scaleX(1);
  }

  .guide-card:hover::after,
  .guide-card:focus-within::after {
    transform: none;
  }

  /* --- Safety net -------------------------------------------------------
     Anything a world stylesheet animates that this file has not named by
     hand. Durations collapse and loops run once; the explicit rules above
     still control which properties end up where. */
  *,
  *::before,
  *::after {
    animation-duration: 1ms;
    animation-delay: 0s;
    animation-iteration-count: 1;
    transition-duration: 1ms;
    transition-delay: 0s;
    scroll-behavior: auto;
  }
}
