/* NOCTURNE motion: tokens, keyframes, pre-animation states. Pairs with motion.js. [motion]
   Every hidden state is scoped to html.ns-motion, which only motion.js sets (never under reduced motion), so without
   JS nothing is ever hidden. Animated properties: transform (and its translate / scale longhands), opacity, clip-path. */

:root {
  --m-1: 120ms;                              /* micro: press, hover */
  --m-2: 200ms;                              /* small: rings, chips */
  --m-3: 320ms;                              /* move: FLIP, a row entering */
  --m-4: 600ms;                              /* reveal, number roll */
  --m-std: cubic-bezier(.2, 0, 0, 1);        /* standard: quick start, long settle */
  --m-emph: cubic-bezier(.05, .7, .1, 1);    /* emphasized: arrivals */
  --m-exit: cubic-bezier(.3, 0, 1, 1);       /* exits */
  --m-rise: 16px;
  --m-wave-step: 24ms;                       /* hours chart: delay per diagonal step */
}

/* Keyframes. The end state is left implicit on purpose: an animation ends on the element's own values,
   so a dimmed or tilted element is never forced to opacity 1 or transform none. */
@keyframes nsRise   { from { opacity: 0; translate: 0 var(--m-rise); } }
@keyframes nsFadeIn { from { opacity: 0; } }
@keyframes nsEnter  { from { opacity: 0; translate: 0 -12px; } }
@keyframes nsPop    { 0% { opacity: 0; scale: 0; } 55% { scale: 1.4; } }
@keyframes nsWipe   { from { clip-path: inset(-12px calc(100% + 12px) -12px -12px); } to { clip-path: inset(-12px -12px -12px -12px); } }
@keyframes nsWaveOn { 0% { opacity: .1; scale: .8; } 45% { opacity: 1; scale: 1.1; } }

@media (prefers-reduced-motion: no-preference) {
  /* reveal: held invisible until 20% on screen, then fade + 16px up (motion.js) */
  html.ns-motion [data-reveal]:not([data-reveal="stagger"]):not([data-in]),
  html.ns-motion [data-reveal="stagger"]:not([data-in]) > *,
  /* hero intro: only while html.ns-intro is set, so a late or broken intro can never keep the hero hidden */
  html.ns-motion.ns-intro [data-intro]:not([data-intro^="lines"]):not([data-intro^="lift"]):not([data-in]),
  html.ns-motion.ns-intro [data-intro^="lines"]:not([data-in]) > *,
  /* chart draw: endpoints wait for the line */
  html.ns-motion [data-draw]:not([data-drawn]) [data-draw-end] { opacity: 0 !important; }
  html.ns-motion [data-draw]:not([data-drawn]) svg { clip-path: inset(0 100% 0 0); }

  /* weekly hours chart: dark until seen, then every cell lights up along the diagonal (--i set by motion.js) */
  html.ns-motion [data-wave]:not([data-lit]) [data-w] { opacity: .1; scale: .8; }
  html.ns-motion [data-wave][data-lit] [data-w] { animation: nsWaveOn var(--m-4) var(--m-emph) calc(var(--wave-delay, 0ms) + var(--i, 0) * var(--m-wave-step)) backwards; }
}

/* Orbit. motion.js writes transform, z-index and --orb-dim on each [data-orb]; the template writes the same values
   once, statically, so no JS or reduced motion shows the resting layout. */
[data-orbit] [data-orb] { will-change: transform; }
[data-orb]::after { content: ""; position: absolute; inset: 0; border-radius: 50%; background: #000; opacity: var(--orb-dim, 0); pointer-events: none; }
[data-orb]::before { content: ""; position: absolute; inset: -7%; border-radius: 50%; box-shadow: 0 0 0 1px rgba(255, 255, 255, .85); opacity: 0; transition: opacity var(--m-2) var(--m-std); pointer-events: none; }
[data-orb]:hover::before, [data-orb]:focus-visible::before { opacity: 1; }
[data-orb]:focus-visible { outline: none; }

@media (prefers-reduced-motion: reduce) {
  [data-orb]::before { transition: none; }
  [data-wave] [data-w] { animation: none !important; }
}
