/* assets/rn/motion.css — one motion language for the whole site (1-motion-system). Loaded after base.css.
   Everything moves on the hero's springs (tokens.css --rn-ease-* / --rn-dur-*, 108 BPM), on transform and opacity
   only (SVG stroke draws are the one exception), once, when it enters the viewport. Nothing loops offscreen, the
   site pause (html[data-motion=paused]) shows every final state, and prefers-reduced-motion shows final states
   with no movement at all. assets/rn/motion.js drives the data-rn-* primitives; overhaul/MOTION.md documents them.

   1. Reveal (.reveal and [data-rn-reveal])      5. Toolkit primitives (kinetic, reel, draw, rows)
   2. The guard layer (pause, offscreen, loops)  6. Hover and press
   3. Same-document swaps (filters)              7. The pause toggle
   4. Reduced motion                                                                                              */

/* 1. Reveal ─────────────────────────────────────────────────────────────────────────────────────────────────
   One-shot animations, not transitions, so a component's own `transition` shorthand can never drop the fade.
   The rise uses the independent `translate` property, so it composes with any component transform.
   html.js is set by the first <head> script; without it (no JavaScript) nothing is ever hidden. Never inside the
   hero.
   The failsafe shows the content after 4s, but only while the scripts that reveal it have not gone live: site.js
   sets html.rn-reveals-live once its observer watches the page's .reveal elements, and motion.js sets
   html.rn-toolkit-live once it has booted (it takes over .reveal when site.js never went live). A live page never
   pops an element in at 4s, so an element peeking in at the bottom stays hidden until it triggers, then fades
   once. A script that goes live after the failsafe has fired keeps what it showed (.rn-shown: no second fade). */
html.js :is(.reveal, [data-rn-reveal]):not(.is-visible):not(.lux-hero *) {
  opacity: 0;
}

html.js :is(.reveal, [data-rn-reveal]).is-visible:not(.lux-hero *) {
  animation:
    rn-reveal-fade var(--rn-dur-fade) var(--rn-ease-critical) backwards,
    rn-reveal-rise var(--rn-dur-ui) var(--rn-ease-ui) backwards;
  animation-delay: calc(min(var(--reveal-i, 0), 5) * var(--rn-stagger) + var(--rn-delay, 0ms));
}

/* A timeline item with no primitive of its own (data-rn-beat only) is revealed on its beat. */
html.js [data-rn-timeline] [data-rn-beat]:not([data-rn-kinetic], [data-rn-reel], [data-rn-draw], [data-rn-stagger-rows], .is-visible) {
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  html.js:not(.rn-reveals-live, .rn-toolkit-live) .reveal:not(.is-visible):not(.lux-hero *),
  html.js:not(.rn-toolkit-live) [data-rn-reveal]:not(.is-visible):not(.lux-hero *),
  html.js:not(.rn-toolkit-live) [data-rn-timeline] [data-rn-beat]:not([data-rn-kinetic], [data-rn-reel], [data-rn-draw], [data-rn-stagger-rows], .is-visible) {
    animation: rn-failsafe 1ms 4s forwards;
  }
}

html.js [data-rn-reveal="fade"].is-visible:not(.lux-hero *) {
  animation: rn-reveal-fade var(--rn-dur-fade) var(--rn-ease-critical) backwards;
  animation-delay: calc(min(var(--reveal-i, 0), 5) * var(--rn-stagger) + var(--rn-delay, 0ms));
}

html.js :is(.reveal, [data-rn-reveal]).is-visible.rn-shown:not(.lux-hero *) {
  animation: none;
}

@keyframes rn-failsafe {
  to {
    opacity: 1;
    scale: none;
  }
}

@keyframes rn-reveal-fade {
  from {
    opacity: 0;
  }
}

@keyframes rn-reveal-rise {
  from {
    translate: 0 var(--rn-reveal-distance);
  }
}

/* 2. The guard layer ─────────────────────────────────────────────────────────────────────────────────────────
   Declared first on site.css line 1, so its !important beats every other !important (legacy included). */
@layer rn-motion-guard {
  /* stylelint-disable declaration-no-important -- the guard layer is the one place !important belongs */
  /* Nothing loops offscreen (motion.js marks sections outside the viewport + 100px), and the site pause stops
     every CSS animation. Script animations are handled by their owners (the hero, motion.js, the rune). */
  .is-offscreen *,
  .is-offscreen *::before,
  .is-offscreen *::after,
  html[data-motion="paused"] *,
  html[data-motion="paused"] *::before,
  html[data-motion="paused"] *::after {
    animation-play-state: paused !important;
  }

  /* A paused visitor still gets the content: reveals skip straight to their final state. */
  html[data-motion="paused"] :is(.reveal, [data-rn-reveal], [data-rn-timeline] [data-rn-beat]) {
    opacity: 1 !important;
    animation: none !important;
  }

  /* So does the brief's one-shot step entrance: paused on its first frame it would hold the step invisible. Once
     motion.js is live (html.rn-pause-finish) it jumps every finite CSS animation to its end while paused
     (settlePaused), which a resume does not replay; until then, and if it never loads, it is switched off.
     (The decorative loops retired in stage 1 lost their markup in stage 3; their entries here went in 4-consolidation.) */
  html[data-motion="paused"]:not(.rn-pause-finish) .brief-step.is-active {
    animation: none !important;
  }
  /* stylelint-enable declaration-no-important */
}

/* 3. Same-document swaps (motion.js swap(): filter changes). Scoped to :root.rn-swap, which exists only while a
   swap runs, so page-to-page navigation is never touched. The page itself does not cross-fade; only the items
   named for the swap move (group: UI spring) and fade (critical), with the hero's short blur on exit where a
   hover-capable device can afford it. */
:root.rn-swap::view-transition-group(*) {
  animation-duration: var(--rn-dur-ui);
  animation-timing-function: var(--rn-ease-ui);
}

:root.rn-swap::view-transition-old(*),
:root.rn-swap::view-transition-new(*) {
  animation-duration: var(--rn-dur-fade);
  animation-timing-function: var(--rn-ease-critical);
}

:root.rn-swap::view-transition-old(*) {
  animation-name: rn-swap-out;
}

:root.rn-swap::view-transition-new(*) {
  animation-name: rn-swap-in;
}

:root.rn-swap::view-transition-old(root) {
  animation: none;
  opacity: 0;
}

:root.rn-swap::view-transition-new(root) {
  animation: none;
}

@keyframes rn-swap-out {
  to {
    opacity: 0;
    filter: blur(var(--rn-swap-blur));
  }
}

@keyframes rn-swap-in {
  from {
    opacity: 0;
  }
}

/* 4. Reduced motion: final states at once, no movement anywhere (the toggle is hidden too, section 7). The site pause
   is the same final state, so in-page jumps and scripted scrolls (behavior "auto") jump instead of gliding under it
   (the legacy html { scroll-behavior: smooth } in site.css and studio.css resets only for reduced motion). */
html[data-motion="paused"] {
  scroll-behavior: auto;
}

@media (prefers-reduced-motion: reduce) {
  html.js :is(.reveal, [data-rn-reveal]):not(.is-visible):not(.lux-hero *),
  html.js :is(.reveal, [data-rn-reveal]).is-visible:not(.lux-hero *),
  html.js [data-rn-reveal="fade"].is-visible:not(.lux-hero *),
  html.js [data-rn-timeline] [data-rn-beat],
  html.js [data-rn-timeline] [data-rn-beat]:not([data-rn-kinetic], [data-rn-reel], [data-rn-draw], [data-rn-stagger-rows], .is-visible) {
    opacity: 1;
    animation: none;
  }
}

/* 5. Toolkit primitives ──────────────────────────────────────────────────────────────────────────────────────
   Before motion.js plays a primitive it is held at its first frame (identical to the first keyframe, so there is
   no jump), unless the visitor prefers reduced motion or has paused motion. Each hold has the 4s failsafe until
   motion.js has booted (html.rn-toolkit-live, section 1). */
@media (prefers-reduced-motion: no-preference) {
  html.js:not([data-motion="paused"]) :is([data-rn-kinetic], [data-rn-reel], [data-rn-draw="cover"], svg[data-rn-draw], [data-rn-draw] svg):not(.rn-in),
  html.js:not([data-motion="paused"]) [data-rn-stagger-rows]:not(.rn-in) > * {
    opacity: 0;
  }

  html.js:not([data-motion="paused"]) [data-rn-draw]:not([data-rn-draw="cover"], [data-rn-draw="y"], svg, :has(svg)):not(.rn-in) {
    scale: 0 1;
  }

  html.js:not([data-motion="paused"]) [data-rn-draw="y"]:not(.rn-in) {
    scale: 1 0;
  }

  html.js:not([data-motion="paused"], .rn-toolkit-live) :is([data-rn-kinetic], [data-rn-reel], [data-rn-draw="cover"], svg[data-rn-draw], [data-rn-draw] svg):not(.rn-in),
  html.js:not([data-motion="paused"], .rn-toolkit-live) [data-rn-stagger-rows]:not(.rn-in) > *,
  html.js:not([data-motion="paused"], .rn-toolkit-live) [data-rn-draw]:not([data-rn-draw="cover"], svg, :has(svg)):not(.rn-in) {
    animation: rn-failsafe 1ms 4s forwards;
  }
}

/* Kinetic type (data-rn-kinetic, headings): words rise into masks on the beat, as in the hero's closing lines.
   The words are the heading's own text, split in place, so it copies as written; assistive tech reads the
   heading's aria-label (motion.js). The masks extend 0.14em above and below the line box without changing it,
   so display line-heights never clip. */
.rn-k-w {
  display: inline-block;
  overflow: hidden;
  padding-block: 0.14em;
  margin-block: -0.14em;
  vertical-align: top;
}

.rn-k-i {
  display: inline-block;
}

/* The optional highlight (data-rn-kinetic="highlight"): a block lands behind the emphasised words, then they
   rise onto it. It is the family signal, or the text colour in the audit family (never red). */
[data-rn-kinetic="highlight"] {
  position: relative;
  isolation: isolate;
}

.rn-k-block {
  position: absolute;
  z-index: -1;
  border-radius: 0.06em;
  background: var(--rn-signal, var(--rn-text));
  transform-origin: 0 50%;
  pointer-events: none;
}

.rn-k-hl > .rn-k-i,
[data-rn-kinetic="highlight"].rn-k-static :is(em, mark) {
  color: var(--rn-on-mark, var(--rn-on-text));
}

/* Under reduced motion the words are never split (.rn-k-static): the blocks are measured over the <em>/<mark>
   text itself, which takes the mark colour. A <mark> never paints its own background over the block. */
[data-rn-kinetic="highlight"] mark {
  background: none;
}

body:is(.studio-work-page, .studio-prototypes-page, .studio-alchemy-page, .studio-blog-page, .studio-books-page),
body.lux-home :is(.lux-ticker, .rn-ticker, .lux-work, .lux-prototypes, .lux-method) {
  --rn-on-mark: var(--rn-ink);
}

[data-tone=red] {
  --rn-on-text: var(--rn-paper);
}

/* Digit reels (data-rn-reel): each digit is a strip that rolls to its value, like the hero's ROAS reel. The text
   itself never changes: while it rolls it is see-through in place (so nothing reflows) under an aria-hidden,
   unselectable copy laid over each character by motion.js, removed when the strips land. Tabular figures keep
   every digit one width. */
[data-rn-reel] {
  font-variant-numeric: tabular-nums;
}

[data-rn-reel].is-rolling {
  -webkit-text-fill-color: transparent;
}

.rn-reel-roll {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  -webkit-text-fill-color: currentColor;
  white-space: pre;
  text-align: left;
  user-select: none;
  pointer-events: none;
}

.rn-reel-roll > span {
  position: absolute;
}

.rn-reel-cell {
  clip-path: inset(0 -0.1em);
}

.rn-reel-strip,
.rn-reel-strip > span {
  display: block;
}

.rn-reel-strip > span {
  margin-bottom: 0.5em;
}

/* Draws (data-rn-draw): a line or bar grows from its start (x: left, y: bottom), a cover slides off
   (data-rn-draw="cover", set --rn-cover to the surface behind), or an SVG stroke draws itself. */
[data-rn-draw]:not([data-rn-draw="y"]) {
  transform-origin: 0 50%;
}

[data-rn-draw="y"] {
  transform-origin: 50% 100%;
}

[data-rn-draw="cover"] {
  position: relative;
  overflow: clip;
}

.rn-draw-cover {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--rn-cover, var(--rn-ink));
  pointer-events: none;
}

.rn-in .rn-draw-cover:not(.is-live),
[data-motion="paused"] .rn-draw-cover {
  display: none;
}

/* Rows (data-rn-stagger-rows): rows land one after another like the hero's findings. A row marked
   data-rn-finding gets the red finding rule, drawn once the row has landed. Red means a finding only. The rule
   is styled from the attribute, so it is there without JavaScript and under reduced motion; motion.js only
   draws it in. */
:is(.rn-finding, [data-rn-stagger-rows] > [data-rn-finding]) {
  position: relative;
}

:is(.rn-finding, [data-rn-stagger-rows] > [data-rn-finding])::before {
  position: absolute;
  top: 0.35em;
  bottom: 0.35em;
  left: calc(-1 * var(--rn-finding-gap, 14px));
  width: 3px;
  border-radius: var(--rn-radius-pill);
  background: var(--rn-red);
  content: "";
  transform-origin: 50% 0;
}

/* 6. Hover and press: the SNAP spring on transform, opacity and colour. A lift of a couple of pixels and a
   press, never a bounce; only where a pointer can hover. They use the independent translate and scale
   properties, so they compose with any component transform (and the legacy hover transforms are dropped). */
:is(.lux-button, .studio-button, .nav-action, .rn-motion-toggle, [data-rn-press]),
:is(.lux-work-card, .lux-capability-card, .home-audit-grid article, .blog-card, .blog-feature-card, [data-rn-lift]) {
  transition-property: translate, scale, opacity, color, background-color, border-color;
  transition-duration: var(--rn-dur-snap);
  transition-timing-function: var(--rn-ease-snap);
}

:is(.lux-button, .studio-button):is(:hover, :focus-visible),
:is(.home-audit-grid article, .blog-card, .blog-feature-card):hover {
  transform: none;
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  :is(.lux-button, .studio-button, .nav-action, [data-rn-press]):hover {
    translate: 0 -2px;
  }

  :is(.lux-work-card, .lux-capability-card, .home-audit-grid article, .blog-card, .blog-feature-card, [data-rn-lift]):hover {
    translate: 0 -4px;
  }
}

@media (prefers-reduced-motion: no-preference) {
  :is(.lux-button, .studio-button, .nav-action, .rn-motion-toggle, [data-rn-press]):active {
    translate: 0 0;
    scale: 0.98;
  }

  :is(.lux-work-card, .lux-capability-card, .home-audit-grid article, .blog-card, .blog-feature-card, [data-rn-lift]):active {
    translate: 0 -1px;
    scale: 0.995;
  }
}

/* 7. The pause toggle: one control for the whole site, the hero included (motion.js). A constant name and
   aria-pressed, like the hero's own button; the icon shows what a press will do. */
.rn-motion-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-width: var(--rn-target);
  min-height: var(--rn-target);
  margin: 0;
  padding: 0 16px 0 15px;
  border: 1px solid var(--rn-ring);
  border-radius: var(--rn-radius-pill);
  background: transparent;
  color: var(--rn-text);
  font: 500 var(--rn-fs-label)/1 var(--rn-font-mono);
  letter-spacing: var(--rn-track-label);
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
}

.rn-motion-toggle:hover {
  border-color: var(--rn-muted);
}

.rn-motion-toggle[aria-pressed="true"] {
  border-color: var(--rn-text);
  background: var(--rn-text);
  color: var(--rn-on-text);
}

/* Pause bars while motion runs; a play triangle once it is paused. */
.rn-motion-toggle-icon {
  flex: none;
  box-sizing: border-box;
  width: 8px;
  height: 10px;
  border-inline: 2px solid currentColor;
}

.rn-motion-toggle[aria-pressed="true"] .rn-motion-toggle-icon {
  width: 9px;
  height: 10px;
  border: 0;
  background: currentColor;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

.footer-legal .rn-motion-toggle {
  margin-block: -12px;
}

.footer-legal > div:has(> .rn-motion-toggle) {
  align-items: center;
}

@media (prefers-reduced-motion: reduce) {
  .rn-motion-toggle {
    display: none;
  }
}

@media (forced-colors: active) {
  .rn-motion-toggle[aria-pressed="true"] {
    border: 2px solid ButtonText;
  }
}
