/* ============================================================
   ERGÖK — MOTION LAYER  (cascade position 4 of 7)

   Loads AFTER theme.css, BEFORE chrome.css / home.css / pages.css.
   Pairs with public/js/motion.js — the runtime adds the state
   classes / custom properties this file reacts to. See DESIGN.md
   section 6 for the attribute contract.

   HOUSE RULES
   - Consume motion tokens (--dur-*, --ease-*, --stagger) from
     theme.css. Never redefine them here.
   - Entrances animate opacity + transform (+ mask/filter) ONLY.
     Never width/height/top/left.
   - Everything in this file must be inert under
     `prefers-reduced-motion: reduce` (see section 9), and no page
     may be left with invisible content when motion is off.
   - Selector namespace owned here, and nowhere else:
       [data-anim*] [data-parallax] [data-tilt] [data-magnetic]
       [data-marquee] [data-count-to] [data-split]
       .split-* .marquee-* .anim-* .scroll-progress
       html[data-scrolled] / html[data-scroll-dir]

   SECTIONS
     1. runtime knobs
     2. entrance animations  ([data-anim] → .is-inview)
     3. split text           ([data-split] → .split-unit)
     4. counters             ([data-count-to])
     5. parallax             ([data-parallax])
     6. pointer effects      ([data-tilt] [data-magnetic])
     7. ambient utilities    (.anim-drift .anim-scan .anim-pulse-ring .anim-draw)
     8. marquee + scroll progress
     9. reduced motion (global kill switch)
   ============================================================ */

/* ------------------------------------------------------------
   1. RUNTIME KNOBS
   Per-element overrides an author may set inline or from a later
   layer. All optional — the defaults are the house values.

     --anim-duration   entrance duration          (default --dur-4)
     --anim-ease       entrance easing            (default --ease-out)
     --anim-delay      author delay               (default 0ms)
     --anim-stagger    set by the runtime for [data-anim-group]
     --anim-distance   travel for fade-up/slide-* (default 1.15rem)
     --split-stagger   per-unit delay for [data-split]
     --marquee-gap / --marquee-duration / --marquee-distance
     --draw-length     path length for .anim-draw (runtime-measured)
   ------------------------------------------------------------ */

/* ------------------------------------------------------------
   2. ENTRANCE ANIMATIONS
   `data-anim="<name>"` holds the *from* state; the runtime adds
   `.is-inview` to release it. `data-anim-repeat` makes the runtime
   remove the class again on exit, so the from-state must always be
   reachable — never put the end state on a bare tag selector.

   Names (DESIGN.md §6):
     fade         opacity only
     fade-up      rise + fade            — the workhorse
     slide-left   enters from the LEFT   (starts at -X, settles at 0)
     slide-right  enters from the RIGHT  (starts at +X, settles at 0)
     scale-in     settles up from 0.965  — cards, media plates
     clip-up      wipe revealing upward  — display type, rules
     mask-reveal  diagonal gradient wipe — imagery, wide surfaces
     blur-in      defocus → focus        — use sparingly, it is costly
   ------------------------------------------------------------ */
/* GATING: every [data-anim] state below is scoped under
   `html[data-motion="on"]`, which the inline <head> script in
   layouts/main.php sets before any stylesheet paints. With scripting
   off the flag never lands, so the from-states never apply and the
   content renders in its final state — no flash, no invisible copy.
   Keep the gate on ALL of them, from-states and to-states alike, so
   the relative specificity inside this section stays as written.

   The `.is-inview` selector is repeated on purpose: it lifts the rule
   so a later layer's own `transition` shorthand on the same element
   (a card with a hover transition, say) cannot swallow the entrance.
   Transitions are read from the after-change style, which is the
   `.is-inview` one. */
html[data-motion="on"] [data-anim],
html[data-motion="on"] [data-anim].is-inview {
  --anim-distance: 1.15rem;
  transition-property: opacity, transform, filter, clip-path, -webkit-mask-position, mask-position;
  transition-duration: var(--anim-duration, var(--dur-4));
  transition-timing-function: var(--anim-ease, var(--ease-out));
  transition-delay: calc(var(--anim-delay, 0ms) + var(--anim-stagger, 0ms));
}

/* NOTE: deliberately no `will-change` here. A long page carries dozens
   of [data-anim] nodes and pre-promoting all of them costs more memory
   than the transition saves; opacity/transform transitions are
   composited anyway once they start. Only the continuously animating
   things below (parallax, marquee, drift) declare it. */

/* --- from-states (gated) --- */
html[data-motion="on"] [data-anim="fade"] {
  opacity: 0;
}

html[data-motion="on"] [data-anim="fade-up"] {
  opacity: 0;
  transform: translate3d(0, var(--anim-distance), 0);
}

html[data-motion="on"] [data-anim="slide-left"] {
  opacity: 0;
  transform: translate3d(calc(var(--anim-distance) * -1.6), 0, 0);
}

html[data-motion="on"] [data-anim="slide-right"] {
  opacity: 0;
  transform: translate3d(calc(var(--anim-distance) * 1.6), 0, 0);
}

html[data-motion="on"] [data-anim="scale-in"] {
  opacity: 0;
  transform: scale(0.965);
}

html[data-motion="on"] [data-anim="clip-up"] {
  opacity: 0;
  clip-path: inset(100% 0 0 0);
  transform: translate3d(0, calc(var(--anim-distance) * 0.5), 0);
}

/* Per-name timing overrides. Both selectors again, for the same
   after-change-style reason as above. */
html[data-motion="on"] [data-anim="clip-up"],
html[data-motion="on"] [data-anim="clip-up"].is-inview {
  transition-timing-function: var(--anim-ease, var(--ease-emphasis));
}

html[data-motion="on"] [data-anim="mask-reveal"],
html[data-motion="on"] [data-anim="mask-reveal"].is-inview {
  transition-duration: var(--anim-duration, var(--dur-5));
  transition-timing-function: var(--anim-ease, var(--ease-emphasis));
}

html[data-motion="on"] [data-anim="mask-reveal"] {
  opacity: 0;
  -webkit-mask-image: linear-gradient(100deg, #000 38%, rgba(0, 0, 0, 0) 64%);
  mask-image: linear-gradient(100deg, #000 38%, rgba(0, 0, 0, 0) 64%);
  -webkit-mask-size: 280% 100%;
  mask-size: 280% 100%;
  -webkit-mask-position: 100% 0;
  mask-position: 100% 0;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

html[data-motion="on"] [data-anim="blur-in"] {
  opacity: 0;
  filter: blur(10px);
  transform: translate3d(0, calc(var(--anim-distance) * 0.4), 0);
}

/* --- to-states: one rule per property family --- */
html[data-motion="on"] [data-anim].is-inview {
  opacity: 1;
  transform: none;
  filter: none;
}

html[data-motion="on"] [data-anim="clip-up"].is-inview {
  clip-path: inset(0 0 0 0);
}

html[data-motion="on"] [data-anim="mask-reveal"].is-inview {
  -webkit-mask-position: 0 0;
  mask-position: 0 0;
}

/* A bare `data-anim` with no value is still a fade. */
html[data-motion="on"] [data-anim=""] {
  opacity: 0;
}

/* Belt-and-braces on top of the html[data-motion="on"] gate above:
   scripting off cancels the from-states outright. The runtime also
   force-reveals on error (motion.js `panic()`), and site.js releases
   every [data-anim] if motion.js never arrives. Three layers, because
   invisible content is the one failure mode we cannot ship. */
@media (scripting: none) {
  [data-anim] {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
    -webkit-mask-image: none !important;
    mask-image: none !important;
    transition: none !important;
  }
}

/* ------------------------------------------------------------
   3. SPLIT TEXT
   `data-split="chars|words|lines"` → the runtime rewrites the text
   as <span class="split-word"><span class="split-unit" style="--i:N">
   and mirrors the original string into aria-label, so assistive tech
   reads the sentence, not the alphabet.

   Pair with `data-anim` on the same element: the parent's own
   from-state is cancelled (higher specificity below) and the units
   animate instead, staggered by --i.
   ------------------------------------------------------------ */
[data-split] {
  --split-stagger: 30ms;
}

[data-split="chars"] {
  --split-stagger: 26ms;
}

[data-split="words"] {
  --split-stagger: 52ms;
}

[data-split="lines"] {
  --split-stagger: 88ms;
}

/* Keeps a word from breaking mid-unit when characters are split. */
.split-word {
  display: inline-block;
  white-space: pre-wrap;
}

.split-unit {
  display: inline-block;
}

/* Transient: the runtime wraps words in these only to read their line
   boxes when splitting by line, then replaces them. Must stay inline. */
.split-probe {
  display: inline;
}

/* `lines` produces block-level units so each line rises as one. */
[data-split="lines"] .split-unit {
  display: block;
}

/* Cancel the container's own entrance — the units carry it. Gated and
   ordered to beat the from-states above (one more attribute, later in
   the file). */
html[data-motion="on"] [data-split][data-anim] {
  opacity: 1;
  transform: none;
  filter: none;
  clip-path: none;
  -webkit-mask-image: none;
  mask-image: none;
}

html[data-motion="on"] [data-anim][data-split] .split-unit {
  opacity: 0;
  transform: translate3d(0, 0.42em, 0);
  transition:
    opacity var(--anim-duration, var(--dur-4)) var(--ease-out),
    transform var(--anim-duration, var(--dur-4)) var(--ease-emphasis);
  transition-delay: calc(
    var(--anim-delay, 0ms) + var(--anim-stagger, 0ms) + var(--i, 0) * var(--split-stagger)
  );
}

html[data-motion="on"] [data-anim][data-split].is-inview .split-unit {
  opacity: 1;
  transform: none;
}

/* ------------------------------------------------------------
   4. COUNTERS
   `data-count-to` — tabular figures so the box never reflows while
   the digits roll.
   ------------------------------------------------------------ */
[data-count-to] {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ------------------------------------------------------------
   5. PARALLAX
   The runtime writes --parallax-y; the transform lives here so a
   later layer can read the variable instead of fighting over
   `transform`. Do NOT put [data-parallax] and [data-anim] on the
   same node — they both own `transform`. Wrap instead.
   ------------------------------------------------------------ */
[data-parallax] {
  transform: translate3d(0, var(--parallax-y, 0px), 0);
  will-change: transform;
}

/* Parallax is a desktop-only affordance (DESIGN.md §5). */
@media (max-width: 767.98px) {
  [data-parallax] {
    transform: none;
    will-change: auto;
  }
}

/* ------------------------------------------------------------
   6. POINTER EFFECTS
   --tilt-x/--tilt-y (deg, ≤6) and --mag-x/--mag-y (px, ≤6) are
   written by the shared pointermove handler. `.is-pointing` shortens
   the transition so the element tracks the cursor, and dropping the
   class lets it glide back to rest.
   ------------------------------------------------------------ */
[data-tilt],
[data-magnetic] {
  transform: translate3d(var(--mag-x, 0px), var(--mag-y, 0px), 0) perspective(900px)
    rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
  transition: transform var(--dur-3) var(--ease-out);
}

[data-tilt].is-pointing,
[data-magnetic].is-pointing {
  transition-duration: var(--dur-1);
}

[data-tilt] {
  transform-style: preserve-3d;
}

/* Coarse pointers and narrow viewports never get these — the runtime
   does not install them, this just neutralises a stale inline value. */
@media (max-width: 767.98px), (pointer: coarse) {
  [data-tilt],
  [data-magnetic] {
    transform: none;
  }
}

/* Buttons already own `transform` for their hover lift (theme.css
   `.btn:hover`). Rather than let one rule cancel the other, fold the
   magnet into the same transform — one attribute more than `.btn:hover`,
   so it wins on specificity and never needs `!important`. */
.btn[data-magnetic]:hover,
.btn[data-magnetic]:focus-visible {
  transform: translate3d(var(--mag-x, 0px), calc(var(--mag-y, 0px) - 2px), 0);
}

/* The outline variant deliberately stays flat — magnet only. */
.btn-outline[data-magnetic]:hover,
.btn-outline[data-magnetic]:focus-visible {
  transform: translate3d(var(--mag-x, 0px), var(--mag-y, 0px), 0);
}

/* No pointer to be magnetic towards: hand the buttons back to theme.css. */
@media (max-width: 767.98px), (pointer: coarse) {
  .btn[data-magnetic]:hover,
  .btn[data-magnetic]:focus-visible {
    transform: translateY(-2px);
  }

  .btn-outline[data-magnetic]:hover,
  .btn-outline[data-magnetic]:focus-visible {
    transform: none;
  }
}

/* ------------------------------------------------------------
   7. AMBIENT UTILITIES
   Reusable looping effects for the composition layers. Each is a
   plain class so home.css / pages.css can opt in per component.
   ------------------------------------------------------------ */

/* .anim-drift — slow aurora/gradient drift.
   Apply to a decorative, absolutely-positioned blob that already has
   a radial/linear gradient background. 24s, never in the reading path.
   Example:  <span class="anim-drift" aria-hidden="true"></span>  */
.anim-drift {
  animation: ergok-drift var(--drift-duration, 24s) var(--ease-in-out) infinite alternate;
  will-change: transform;
}

@keyframes ergok-drift {
  0% {
    transform: translate3d(-2%, -1%, 0) scale(1);
  }
  50% {
    transform: translate3d(2.5%, 1.5%, 0) scale(1.06);
  }
  100% {
    transform: translate3d(-1%, 2%, 0) scale(1.02);
  }
}

/* .anim-scan — a single light sweep crossing the surface, like a
   sensor pass. Needs `position: relative` + `overflow: hidden` on the
   host (it paints through ::after). Long pause between passes so it
   reads as instrumentation, not as a shimmer.
   Example:  <div class="panel anim-scan"> … </div>  */
.anim-scan {
  position: relative;
}

.anim-scan::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 42%;
  pointer-events: none;
  background: var(--grad-sheen);
  opacity: 0.5;
  transform: translate3d(-140%, 0, 0) skewX(-14deg);
  animation: ergok-scan var(--scan-duration, 6.5s) var(--ease-in-out) infinite;
}

@keyframes ergok-scan {
  0%,
  62% {
    transform: translate3d(-140%, 0, 0) skewX(-14deg);
  }
  100% {
    transform: translate3d(340%, 0, 0) skewX(-14deg);
  }
}

/* .anim-pulse-ring — soft expanding ring for status dots / hotspots.
   Host must be `position: relative` and is normally a small round dot.
   Two rings offset in time; colour follows currentColor.
   Example:  <span class="dot anim-pulse-ring"></span>  */
.anim-pulse-ring::before,
.anim-pulse-ring::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid currentColor;
  pointer-events: none;
  opacity: 0;
  animation: ergok-pulse-ring var(--pulse-duration, 2.8s) var(--ease-out) infinite;
}

.anim-pulse-ring::after {
  animation-delay: calc(var(--pulse-duration, 2.8s) / 2);
}

@keyframes ergok-pulse-ring {
  0% {
    opacity: 0.55;
    transform: scale(1);
  }
  70% {
    opacity: 0;
    transform: scale(2.6);
  }
  100% {
    opacity: 0;
    transform: scale(2.6);
  }
}

/* .anim-cursor-glow — a brand halo that follows the cursor across a
   surface. Pair with `data-pointer-track` on the same element: the
   runtime writes --pointer-x / --pointer-y (0→1) and toggles
   `.is-pointing`. The host needs `position: relative`, and its ::before
   must be free — components that already use it (.cap-item, .path-card)
   drive their own layer from the same two variables instead.
   Tune with --glow-size / --glow-strength.
   Example:  <a class="card anim-cursor-glow" data-pointer-track> … </a>  */
.anim-cursor-glow {
  position: relative;
  /* Own stacking context, so the halo below can sit at -1: above the
     host's background, under its content, without escaping the card. */
  isolation: isolate;
}

.anim-cursor-glow::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    var(--glow-size, 20rem) circle at calc(var(--pointer-x, 0.5) * 100%) calc(var(--pointer-y, 0.5) * 100%),
    var(--edge-glow),
    transparent 60%
  );
  transition: opacity var(--dur-3) var(--ease-out);
}

.anim-cursor-glow.is-pointing::before {
  opacity: var(--glow-strength, 0.55);
}

/* Touch and narrow viewports never get a cursor — the runtime does not
   install the handler there, so the halo would sit at its 0.5/0.5
   default forever. */
@media (max-width: 767.98px), (pointer: coarse) {
  .anim-cursor-glow::before {
    display: none;
  }
}

/* .anim-draw — draw-in SVG stroke. Put it on the <path>/<line>/etc.
   The runtime measures getTotalLength() into --draw-length; the
   fallback covers paths added after boot. Triggers on .is-inview,
   either on the shape itself or on any ancestor (e.g. the <svg>
   carrying data-anim).
   Example:  <svg data-anim="fade"><path class="anim-draw" …/></svg>  */
.anim-draw {
  stroke-dasharray: var(--draw-length, 1200);
  stroke-dashoffset: var(--draw-length, 1200);
  transition: stroke-dashoffset var(--draw-duration, var(--dur-6)) var(--ease-in-out);
  transition-delay: calc(var(--anim-delay, 0ms) + var(--anim-stagger, 0ms));
}

.anim-draw.is-inview,
.is-inview .anim-draw {
  stroke-dashoffset: 0;
}

/* ------------------------------------------------------------
   8. MARQUEE + SCROLL PROGRESS
   ------------------------------------------------------------ */

/* `data-marquee` on the container; the runtime moves the children
   into .marquee-set and clones the set until it covers 2× the
   container, then sets --marquee-distance / --marquee-duration.
   `data-marquee="reverse"` runs right-to-left, `data-marquee-speed`
   sets px/second (default 55).
   Pauses on hover and on focus-within so keyboard users can reach
   links inside it.
   Put the attribute on a plain wrapper, never directly on a <ul> or
   <table>: the runtime inserts <div>s as its children. */
[data-marquee] {
  display: block;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}

.marquee-track {
  display: flex;
  width: max-content;
  align-items: center;
  gap: var(--marquee-gap, 3rem);
  animation: ergok-marquee var(--marquee-duration, 40s) linear infinite;
  will-change: transform;
}

.marquee-set {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--marquee-gap, 3rem);
}

[data-marquee="reverse"] .marquee-track {
  animation-direction: reverse;
}

[data-marquee]:hover .marquee-track,
[data-marquee]:focus-within .marquee-track {
  animation-play-state: paused;
}

@keyframes ergok-marquee {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    transform: translate3d(calc(var(--marquee-distance, 0px) * -1), 0, 0);
  }
}

/* .scroll-progress — reading-progress rule. Drop it anywhere (header
   underside, top of the viewport) and it fills with --scroll-progress,
   which the runtime keeps on :root.
   Example:  <div class="scroll-progress" aria-hidden="true"></div>

   STACK: 90 by default, chosen against the existing ladder —
   header 40 (styles.css), skip-link 100 (app.css), cookie banner 1000.
   So it paints over the sticky header but never over the skip link or
   the consent banner. If chrome.css lifts the header above 90, retune
   with `--scroll-progress-z` rather than editing this file; if chrome
   would rather own a bar inside the header, set position/z-index on
   its own element and ignore this helper. */
.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--scroll-progress-z, 90);
  height: var(--scroll-progress-h, 2px);
  pointer-events: none;
  background: var(--grad-brand);
  transform: scaleX(var(--scroll-progress, 0));
  transform-origin: left center;
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
}

html[data-scrolled="true"] .scroll-progress {
  opacity: 1;
}

/* ------------------------------------------------------------
   9. REDUCED MOTION — global kill switch
   Everything defined above resolves to its final, legible state.
   The runtime does the same on the JS side (entrances resolve at
   once, counters jump to their value, parallax/tilt/magnetic are
   never installed), so this block is belt-and-braces.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  [data-anim],
  [data-anim].is-inview,
  [data-anim][data-split] .split-unit,
  .split-unit {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
    -webkit-mask-image: none !important;
    mask-image: none !important;
    transition: none !important;
    animation: none !important;
    will-change: auto !important;
  }

  [data-parallax],
  [data-tilt],
  [data-magnetic] {
    transform: none !important;
    transition: none !important;
    will-change: auto !important;
  }

  /* …but a magnetic button still hovers exactly like a plain one. */
  .btn[data-magnetic]:hover,
  .btn[data-magnetic]:focus-visible {
    transform: translateY(-2px) !important;
  }

  .btn-outline[data-magnetic]:hover,
  .btn-outline[data-magnetic]:focus-visible {
    transform: none !important;
  }

  .anim-cursor-glow::before,
  .anim-cursor-glow.is-pointing::before {
    opacity: 0 !important;
    transition: none !important;
  }

  .anim-drift,
  .anim-scan::after,
  .anim-pulse-ring::before,
  .anim-pulse-ring::after,
  .marquee-track {
    animation: none !important;
    transform: none !important;
  }

  .anim-scan::after {
    opacity: 0 !important;
  }

  .anim-draw,
  .anim-draw.is-inview,
  .is-inview .anim-draw {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
    transition: none !important;
  }

  /* The loop is off, so let the row scroll by hand instead. */
  [data-marquee] {
    overflow-x: auto;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .scroll-progress {
    transition: none !important;
  }
}
