/* ARDEN motion.
 *
 * Twenty-one named animations, and the count is not the point — the variety is. A page with
 * one entrance reused everywhere reads as a template no matter how many elements it is applied
 * to, because the eye learns the move on the second card and stops watching. So there are
 * several kinds here and they are assigned by meaning: things that arrive, things that are
 * alive, things that fail, and things that answer you.
 *
 * Every one of them is transform or opacity. Animating width, height or top forces the browser
 * to re-lay-out the page on every frame; on a page with a character grid already running that
 * is the difference between sixty frames and twenty.
 *
 * All of it collapses under prefers-reduced-motion at the bottom of this file. That is not a
 * courtesy — a full-screen field of moving characters is genuinely unpleasant for some people,
 * and the site is unreadable to them otherwise.
 */

/* ── 1. things that arrive ──────────────────────────────────────────────────
   Four entrances, chosen by what the element is rather than by variety for its own sake:
   blocks rise, side matter slides, headlines resolve out of a blur, numbers count up. */
@keyframes a-rise      { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }
@keyframes a-slide     { from { opacity: 0; transform: translateX(-22px); } to { opacity: 1; transform: none; } }
@keyframes a-resolve   { from { opacity: 0; filter: blur(9px); transform: scale(1.02); } to { opacity: 1; filter: none; transform: none; } }
@keyframes a-unfold    { from { opacity: 0; transform: scaleY(.4); } to { opacity: 1; transform: none; } }

/* A wipe that reveals text left to right by moving a mask rather than by changing width, so
   the type never reflows mid-animation. */
@keyframes a-wipe      { from { -webkit-mask-position: 100% 0; mask-position: 100% 0; } to { -webkit-mask-position: 0 0; mask-position: 0 0; } }

/* ── 2. things that are alive ───────────────────────────────────────────────
   Slow, small, and never synchronised — anything that loops in step with something else reads
   as one mechanism, which is exactly wrong for a page describing independent nodes. */
@keyframes a-breathe   { 0%, 100% { opacity: .75; } 50% { opacity: 1; } }
@keyframes a-pulse     { 0% { box-shadow: 0 0 0 0 var(--glow); } 70% { box-shadow: 0 0 0 9px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
@keyframes a-drift     { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes a-blink     { 0%, 45% { opacity: 1; } 50%, 95% { opacity: 0; } 100% { opacity: 1; } }
@keyframes a-flare     { 0%, 100% { text-shadow: 0 0 8px var(--glow); } 50% { text-shadow: 0 0 20px var(--glow); } }
@keyframes a-orbit     { from { transform: rotate(0) translateX(5px) rotate(0); } to { transform: rotate(360deg) translateX(5px) rotate(-360deg); } }

/* ── 3. things that fail ────────────────────────────────────────────────────
   The site is about routes breaking, so failure needs its own vocabulary and it must not look
   like the arrival vocabulary played backwards. These are abrupt, uneven and slightly ugly. */
@keyframes a-tear      { 0%, 92%, 100% { transform: none; } 93% { transform: translateX(-7px); } 95% { transform: translateX(5px); } 97% { transform: translateX(-2px); } }
@keyframes a-glitch    { 0%, 88%, 100% { clip-path: none; transform: none; } 90% { clip-path: inset(18% 0 62% 0); transform: translateX(-6px); } 94% { clip-path: inset(58% 0 22% 0); transform: translateX(6px); } }
@keyframes a-drop      { 0% { transform: none; opacity: 1; } 60% { transform: translateY(6px); opacity: .5; } 100% { transform: translateY(0); opacity: 1; } }
@keyframes a-jitter    { 0%, 100% { transform: none; } 25% { transform: translate(1px, -1px); } 50% { transform: translate(-1px, 1px); } 75% { transform: translate(1px, 1px); } }

/* ── 4. things that answer you ──────────────────────────────────────────────
   Feedback, and all of it under 240ms. Past about 150ms a response stops feeling caused by
   the click that triggered it, and these are the animations a person will see most often. */
@keyframes a-press     { 0% { transform: none; } 45% { transform: translateY(2px) scale(.985); } 100% { transform: none; } }
@keyframes a-confirm   { 0% { transform: scale(1); } 40% { transform: scale(1.12); } 100% { transform: scale(1); } }
@keyframes a-ring      { from { transform: scale(.6); opacity: .7; } to { transform: scale(2.1); opacity: 0; } }
@keyframes a-sweep     { from { transform: translateY(-100%); } to { transform: translateY(320%); } }
@keyframes a-count     { 0% { transform: translateY(60%); opacity: 0; } 100% { transform: none; opacity: 1; } }
@keyframes a-type      { from { width: 0; } to { width: 100%; } }

/* ── application ────────────────────────────────────────────────────────────
   The classes below are what the page actually uses. Kept apart from the keyframes so a new
   element can borrow a movement without a new @keyframes block being invented for it — which
   is how a stylesheet ends up with four names for the same fade. */

/* Scroll entrances. The element is hidden until observed, then animates once. `.in` is added
   by script; without script everything stays visible, which is the correct failure. */
/* Hidden only when script is confirmed alive.
 *
 * This was an unconditional opacity: 0, revealed by an IntersectionObserver. The comment above
 * it claimed that without script everything stays visible; the rule did the opposite. Any
 * failure between page load and the observer running — a module that throws, a browser that
 * never fires idle, a screenshot taken too early — left every section below the hero invisible
 * with the markup all present. A full-page capture is what caught it: the hero, then two
 * thousand pixels of black.
 *
 * The class is set by an inline script in <head>, so it lands before first paint and there is
 * no flash of content that then hides itself. */
html.js [data-rise], html.js [data-slide], html.js [data-resolve], html.js [data-unfold] { opacity: 0; }
[data-rise].in    { animation: a-rise    var(--d-reveal) var(--e-out) both; }
[data-slide].in   { animation: a-slide   var(--d-reveal) var(--e-out) both; }
[data-resolve].in { animation: a-resolve var(--d-scene)  var(--e-out) both; }
[data-unfold].in  { animation: a-unfold  var(--d-reveal) var(--e-out) both; transform-origin: top; }

/* Staggered children. The delay is a custom property set per element by script, so the CSS
   does not need to know how many there are. */
html.js [data-stagger] > * { opacity: 0; }
[data-stagger].in > * { animation: a-rise var(--d-reveal) var(--e-out) both; animation-delay: calc(var(--i, 0) * var(--stagger)); }

/* Live ornament. */
.is-live       { animation: a-breathe 3.4s var(--e-in-out) infinite; }
.dot-live      { animation: a-pulse 2.6s var(--e-out) infinite; }
.is-drift      { animation: a-drift 6s var(--e-in-out) infinite; }
.caret         { animation: a-blink 1.1s steps(1) infinite; }
.is-flare      { animation: a-flare 3.8s var(--e-in-out) infinite; }

/* Failure ornament, applied to the section that talks about failure and nowhere else. */
.is-tear:hover   { animation: a-tear 2.2s steps(1) infinite; }
.is-glitch:hover { animation: a-glitch 1.8s steps(1) infinite; }

/* Feedback. */
.tap:active         { animation: a-press var(--d-feedback) var(--e-out); }
.confirmed          { animation: a-confirm 260ms var(--e-out); }

/* An expanding ring, used once — on the copy button, so a click that copies something
   invisible still produces something visible. */
.ring-fx { position: relative; }
.ring-fx::after {
  content: ''; position: absolute; inset: -2px; border-radius: inherit;
  border: 1px solid var(--lime); opacity: 0; pointer-events: none;
}
.ring-fx.fired::after { animation: a-ring 520ms var(--e-out); }

/* A scanline that crosses a panel on hover. Slow, faint, and only on the panels that carry a
   live readout — on a static card it is decoration, on a live one it reads as refresh. */
.scan { position: relative; overflow: hidden; }
.scan::before {
  content: ''; position: absolute; left: 0; right: 0; height: 34%;
  background: linear-gradient(transparent, color-mix(in srgb, var(--lime) 9%, transparent), transparent);
  transform: translateY(-100%); pointer-events: none;
}
.scan:hover::before { animation: a-sweep 1.5s var(--e-in-out); }

/* Counters. Each digit column animates in on change. */
.count .d { display: inline-block; animation: a-count 320ms var(--e-out); }

/* A caret-terminated typing line, used for the one place the page pretends to be a terminal. */
.typed { display: inline-block; overflow: hidden; white-space: nowrap; animation: a-type 1.4s steps(30, end) both; }

/* ── reduced motion ─────────────────────────────────────────────────────────
   Everything above becomes nothing. Not shortened — off. The tokens already zero the
   durations, but animations declared here name their own, so they are switched off by name. */
@media (prefers-reduced-motion: reduce) {
  [data-rise], [data-slide], [data-resolve], [data-unfold], [data-stagger] > * { opacity: 1 !important; }
  *, *::before, *::after { animation: none !important; }
  .scan::before { display: none; }
}
