/* Explainer diagram chrome — site tokens assumed on :root */
/* --xp-beat: when this element's change lands (ms after the step arrives). Set by
   the runtime per element; not inherited, so a group's beat never leaks to parts
   that changed on their own beat. */
@property --xp-beat { syntax: '<time>'; inherits: false; initial-value: 0s; }
.explainer {
  --xp-pad: 18px;
  /* easings: fades decelerate; moves start from rest and settle (springs sampled
     into linear()); the camera dollies with a soft start and a ~2% overshoot */
  --xp-out: cubic-bezier(.22, 1, .36, 1);
  --xp-glide: linear(0, 0.0004, 0.0049, 0.0202, 0.0526, 0.1055, 0.1794, 0.2717, 0.3771, 0.4884, 0.5962, 0.6918, 0.7709, 0.8335, 0.8812, 0.9167, 0.9426, 0.961, 0.9739, 0.9829, 0.9889, 0.993, 0.9957, 0.9974, 0.9985, 0.9991, 0.9996, 0.9998, 0.9999, 1);
  --xp-settle: linear(0, 0.0004, 0.006, 0.0255, 0.0676, 0.1373, 0.2351, 0.3564, 0.4924, 0.6308, 0.7571, 0.8609, 0.9385, 0.9912, 1.0229, 1.0383, 1.0425, 1.0395, 1.0329, 1.0249, 1.0171, 1.0105, 1.0053, 1.0016, 0.9993, 0.998, 0.9976, 0.9976, 0.9979, 0.9984, 0.9988, 0.9992, 1);
  --xp-dolly: linear(0, 0.0002, 0.0024, 0.0103, 0.028, 0.0588, 0.1046, 0.1657, 0.2412, 0.3285, 0.4246, 0.5251, 0.6254, 0.7207, 0.8056, 0.876, 0.9303, 0.9694, 0.9954, 1.0111, 1.019, 1.0216, 1.0207, 1.018, 1.0145, 1.0109, 1.0076, 1.0049, 1.0028, 1.0013, 1.0002, 0.9996, 1);
  border: 1px solid var(--rule); border-radius: 4px; background: var(--card);
  padding: 13px var(--xp-pad) 0; margin: 28px 0 36px;
}
.explainer:focus { outline: none; }
.explainer:focus-visible { outline: 1.5px solid var(--accent-soft); outline-offset: 2px; }

/* title bar: name left, step count right */
.xp-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
  font-family: var(--mono); font-size: 10px; font-weight: 500;
  letter-spacing: 2px; text-transform: uppercase; color: var(--muted);
  margin-bottom: 12px;
}
.xp-count { letter-spacing: .5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.xp-count b { font-weight: 500; color: var(--ink); }

.explainer > svg { width: 100%; height: auto; display: block; }
.explainer > svg text { font-family: var(--mono); }
/* Step tweens. Each change starts on its element's --xp-beat (the runtime puts
   exits first, then moves, then entrances rippling top-to-bottom, and phrase-
   landed changes on their phrase's beat). --i (inherited) adds a stagger within
   a group: style="--i:3" delays the fourth member 3 × --stagger. Hidden parts
   rest 3px low (`translate`), so entrances rise as they fade in. */
.explainer > svg * {
  transform-box: fill-box; transform-origin: center;
  transition-property: opacity, transform, fill, stroke, translate;
  transition-duration: .45s, .7s, .35s, .35s, .5s;
  transition-timing-function: var(--xp-out), var(--xp-glide), ease, ease, var(--xp-out);
  transition-delay: calc(var(--xp-beat, 0s) + var(--i, 0) * var(--stagger, 45ms));
}

/* caption: every step's text stacked in one grid cell (ghosts invisible),
   so the box is as tall as the longest caption and never jumps */
.xp-caption {
  display: grid; margin-top: 16px; padding: 2px 0 2px 14px;
  border-left: 2px solid var(--accent);
  font-family: var(--sans); font-size: 14.5px; line-height: 1.7; color: var(--body);
}
.xp-caption > * { grid-area: 1 / 1; }
.xp-ghost { visibility: hidden; }
/* the outgoing line leaves first; the new one arrives once the figure has settled (--xp-t) */
.xp-old { animation: xp-cap-out .2s ease-in both; }
@keyframes xp-cap-out { to { opacity: 0; } }
.xp-line.xp-in { animation: xp-cap-in .38s var(--xp-out) var(--xp-t, 0s) both; }
@keyframes xp-cap-in { from { opacity: 0; transform: translateY(3px); } }
/* reading sweep: words light one by one at reading pace (--wi rides MS_WORD),
   like following a subtitle. A word lights to --w-to (ink; accent inside the
   term), swells 2.2%, and relaxes to --w-end when the phrase's beat passes.
   Colour + transform only — nothing reflows. */
.xp-term { color: var(--ink); }
.xp-waiting { color: var(--muted); }
.xp-ph { --w-to: var(--ink); --w-end: var(--body); }
.xp-term { --w-to: var(--accent); --w-end: var(--ink); }
@keyframes xp-w-read { from { color: var(--muted); } to { color: var(--w-to); } }
@keyframes xp-w-relax { to { color: var(--w-end); } }
.explainer:not(.xp-live) .xp-caption * { animation-play-state: paused; }

/* playback bar: full-bleed and ruled — transport cells left; dots + replay right */
.xp-controls {
  display: flex; align-items: stretch; justify-content: space-between; gap: 8px;
  margin: 14px calc(-1 * var(--xp-pad)) 0; border-top: 1px solid var(--rule);
}
.xp-transport, .xp-end { display: flex; }
.xp-btn {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 1;
  letter-spacing: .2px; color: var(--muted); white-space: nowrap;
  background: none; border: 0; border-right: 1px solid var(--rule); border-radius: 0;
  padding: 12px 14px; cursor: pointer; transition: color .15s, background-color .15s;
}
.xp-btn:first-child { padding-left: var(--xp-pad); }
.xp-end .xp-btn { border-right: 0; border-left: 1px solid var(--rule); padding-right: var(--xp-pad); }
.xp-btn:hover:not([aria-disabled="true"]) { color: var(--accent); background: rgba(142, 28, 47, .035); }
.xp-btn[aria-disabled="true"] { color: #cdc6bf; cursor: default; }
.xp-btn:focus-visible, .xp-dot:focus-visible { outline: 1.5px solid var(--accent-soft); outline-offset: -3px; }
/* Play ↔ Pause share one grid cell sized to the wider word: no reflow on toggle */
.xp-play .xp-lbl, .xp-pause .xp-lbl { display: inline-grid; }
.xp-lbl > span { grid-area: 1 / 1; }
.xp-pause .xp-when-paused, .xp-play .xp-when-playing { visibility: hidden; }

/* glyphs drawn in CSS/SVG: identical size and baseline in every font */
.xp-ico { width: 6px; height: 8px; flex: none; background: currentColor; }
.xp-back .xp-ico { clip-path: polygon(100% 0, 0 50%, 100% 100%); }
.xp-next .xp-ico, .xp-play .xp-ico { clip-path: polygon(0 0, 100% 50%, 0 100%); }
.xp-pause .xp-ico { background: none; border-left: 2px solid currentColor; border-right: 2px solid currentColor; }
.xp-replay .xp-ico { width: 10px; height: 10px; background: none; }
.xp-replay .xp-ico svg { display: block; width: 100%; height: 100%; }

/* step dots; the current one is a pill that fills while autoplay holds it */
.xp-dots { display: flex; align-items: center; padding: 0 6px; }
.xp-dot {
  position: relative; width: 16px; align-self: stretch; min-height: 34px;
  border: 0; padding: 0; background: none; cursor: pointer; transition: width .3s cubic-bezier(.22,1,.36,1);
}
.xp-dot::before, .xp-dot::after {
  content: ''; position: absolute; left: 5px; right: 5px; top: 50%; height: 6px; margin-top: -3px;
  border-radius: 3px; background: var(--rule); transition: background-color .2s;
}
.xp-dot.done::before { background: #c9c2bb; }
.xp-dot:hover::before { background: var(--muted); }
.xp-dot.on { width: 32px; }
.xp-dot.on::before { background: var(--accent); }
.xp-dot::after { background: var(--accent); opacity: 0; }
.xp-playing .xp-dot.on::before { background: #e3cfd2; }
.xp-playing .xp-dot.on::after { opacity: 1; animation: xp-fill var(--xp-dwell, 2400ms) linear both; }
@keyframes xp-fill { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }

@media (max-width: 420px) {
  .xp-replay .xp-lbl, .xp-back .xp-lbl { display: none; }
  .xp-controls { gap: 4px; }
  .explainer { --xp-pad: 14px; }
  .xp-head { letter-spacing: 1.2px; gap: 12px; } /* long titles stay on one line */
  /* the diagram itself takes the panel's side padding too: every pixel counts at 1:2 */
  .explainer > svg { width: calc(100% + 2 * var(--xp-pad) - 8px); margin-left: calc(4px - var(--xp-pad)); }
  .xp-btn { padding-left: 10px; padding-right: 10px; gap: 6px; }
  .xp-dots { padding: 0 3px; }
  .xp-dot { width: 13px; }
  .xp-dot::before, .xp-dot::after { left: 4px; right: 4px; }
  .xp-dot.on { width: 26px; }
}

/* kinetic words: each word of a lighting phrase swells ~4% and settles,
   rippling 30ms word to word; transform only, so the line never reflows */
.xp-w { display: inline-block; transform-origin: 50% 85%; }
.xp-beats .xp-ph .xp-w {
  color: var(--muted);
  animation: xp-w-read .35s ease-out calc(var(--at) + var(--wi, 0ms)) both,
             xp-w-swell .7s cubic-bezier(.37,0,.63,1) calc(var(--at) + var(--wi, 0ms)) both,
             xp-w-relax .8s ease-in-out var(--off) forwards;
}
.xp-nomotion .xp-w { color: var(--body); }
.xp-nomotion .xp-term .xp-w { color: var(--ink); }
@keyframes xp-w-swell { 0% { transform: scale(1); } 45% { transform: scale(1.022); } 100% { transform: scale(1); } }

/* speed control: text-only button */
.xp-spd .xp-ico { display: none; }
.xp-spd .xp-lbl { min-width: 2.5ch; letter-spacing: .5px; font-variant-numeric: tabular-nums; }
.xp-spd.on { color: var(--accent); }
.xp-nomotion .xp-spd { display: none; }

/* reduced motion (and Replay's instant cut): no tweens, full captions */
.xp-cut > svg *, .xp-cut .xp-dot,
.xp-nomotion > svg *, .xp-nomotion .xp-caption *, .xp-nomotion .xp-dot,
.xp-nomotion .xp-dot::before, .xp-nomotion .xp-dot::after { transition: none !important; animation: none !important; }

/* ---- shared SVG vocabulary ----
   Stroke weight is hierarchy: hero (accent 1.75) > structure (body 1.25)
   > container (warm grey 1) > guide (dashed, faint 1). Type never goes below
   10 viewBox units: .ttl names things, .sub/.ann annotate them. Text lives in
   reserved zones (header bands, gutters, anchored offsets) that nothing else
   may cross at any step. */
.explainer .panel { fill: none; stroke: #cdc6bf; stroke-width: 1; }
.explainer .panel.fill { fill: #fdfcfb; }
.explainer .panel.hot { stroke: var(--accent); stroke-width: 1.5; }
.explainer .node { fill: #fdfcfb; stroke: var(--body); stroke-width: 1.25; }
.explainer .node.dim { fill: #f6f3f0; stroke: #a8a19a; stroke-width: 1; stroke-dasharray: 4 3; }
.explainer .node.hot { stroke: var(--accent); stroke-width: 1.75; }
.explainer .node.gold { stroke: var(--gold); stroke-width: 1.5; }
.explainer .hd { fill: #f0ece8; }
.explainer .hd.hot { fill: #f4e8ea; }
.explainer .hd.gold { fill: #f3eedf; }
.explainer .hd-rule { stroke: #d8d2cc; stroke-width: 1; }
.explainer .ttl { fill: var(--ink); font-size: 11px; font-weight: 500; letter-spacing: 1px; }
.explainer .sub { fill: var(--muted); font-size: 10px; letter-spacing: .4px; }
.explainer .ann { fill: var(--body); font-size: 10px; letter-spacing: .2px; }
.explainer .num { font-variant-numeric: tabular-nums; }
.explainer .sup { letter-spacing: -2.2px; } /* mono superscripts get a full cell; close the gap */
.explainer .code { fill: var(--body); font-size: 11px; }
.explainer :is(text, tspan).dim { fill: #a8a19a; }
.explainer :is(text, tspan).hot { fill: var(--accent); }
.explainer :is(text, tspan).gold { fill: var(--gold); }
.explainer :is(text, tspan).body { fill: var(--body); }
.explainer .rule { stroke: #e6e1dc; stroke-width: 1; }
.explainer .guide { fill: none; stroke: #cdc6bf; stroke-width: 1; stroke-dasharray: 2 3; }
.explainer .lane { fill: none; stroke: #cdc6bf; stroke-width: 1; stroke-dasharray: 3 4; }
.explainer .lane.on { stroke: #a8a19a; stroke-dasharray: none; }
.explainer .lane.hot { stroke: var(--accent-soft); stroke-dasharray: none; stroke-width: 1.25; }
.explainer .lane.gold { stroke: var(--gold); stroke-dasharray: 3 3; stroke-width: 1.25; }
.explainer .tick { stroke: var(--body); stroke-width: 1.25; }
.explainer .tick.minor { stroke: #cdc6bf; stroke-width: 1; }
.explainer .tick.hot { stroke: var(--accent); stroke-width: 1.75; }
.explainer .tick.gold { stroke: var(--gold); stroke-width: 1.5; }
.explainer .track { fill: #ebe7e3; }
.explainer .seg { fill: #d6cfc8; }
.explainer .seg.on { fill: #b8b0a8; }
.explainer .seg.hot { fill: #ead5d9; stroke: var(--accent); stroke-width: 1; }
/* empty cells are a transparent tint, not `none`, so filling one tweens (none → colour can't) */
.explainer .cell { fill: rgba(179, 171, 163, 0); stroke: #d3ccc5; stroke-width: 1; }
.explainer .cell.on { fill: #b3aba3; stroke: #b3aba3; }   /* data at rest: neutral, so the accent stays free for the hero */
.explainer .cell.gold { fill: #c4ad6b; stroke: var(--gold); }
.explainer .chip { fill: #fff; stroke: #a8a19a; stroke-width: 1; }
.explainer .chip.hot { fill: #f4e8ea; stroke: var(--accent); stroke-width: 1.5; }
.explainer .chip.gold { fill: #f3eedf; stroke: var(--gold); stroke-width: 1.25; }
.explainer .chip.dim { fill: none; stroke: #cdc6bf; stroke-dasharray: 3 2; }
.explainer .bar { fill: var(--accent); }
.explainer .bar.gold { fill: var(--gold); }
.explainer .bar.ghost { fill: none; stroke: #bfb8b1; stroke-dasharray: 3 3; }
.explainer .outline { fill: none; stroke: #bfb8b1; stroke-width: 1; stroke-dasharray: 5 4; }
.explainer .outline.hot { stroke: var(--accent); stroke-width: 1.5; }
.explainer .strike { stroke: var(--gold); stroke-width: 1.5; }
.explainer .dot { fill: var(--body); }
.explainer .dot.hot { fill: var(--accent); }
.explainer .pkt { fill: var(--accent); }
.explainer .pkt.soft { fill: var(--accent-soft); }
.explainer .pkt.gold { fill: var(--gold); }
.explainer .grow { transform-origin: left center; }
/* arrivals land with a slight overshoot-and-settle (≈4%) instead of a dead stop */
.explainer .settle { transition-timing-function: var(--xp-out), var(--xp-settle), ease, ease, var(--xp-out); }

/* ---- motion primitives (all frozen by .xp-nomotion) ----
   Comet packet riding a path — the head eases out, accelerates, and settles
   into its target; at peak speed the group stretches along its heading (a
   velocity smear) and a gradient tail + afterimage dots trail behind:
     <g class="mv" style="offset-path:path('M…'); offset-distance:40%; --dur:2s; --delay:-.7s">
       <path class="tail" d="M1-2.6-18-.6v1.2L1 2.6z" fill="url(#…)"/>
       <circle class="pkt" r="2.6" cx="-7" opacity=".3"/><circle class="pkt" r="3"/></g>
   The inline offset-distance is where it rests under reduced motion. */
.explainer .mv {
  transform-box: view-box; transform-origin: 0 0; offset-rotate: auto;
  animation: xp-travel var(--dur, 2s) cubic-bezier(.45,.05,.3,1) calc(var(--xp-beat, 0s) + var(--delay, 0s)) infinite;
}
@keyframes xp-travel {
  0%   { offset-distance: 0%; opacity: 0; transform: scaleX(.7); }
  12%  { opacity: 1; }
  50%  { transform: scaleX(1.35); }
  84%  { opacity: 1; }
  100% { offset-distance: 100%; opacity: 0; transform: scaleX(.75); }
}
/* landing ping: starts its cycle as the matching comet arrives (same --dur/--delay) */
.explainer .ping { opacity: 0; animation: xp-ping var(--dur, 2s) cubic-bezier(.2,.7,.3,1) calc(var(--xp-beat, 0s) + var(--delay, 0s)) infinite; }
@keyframes xp-ping {
  0%   { opacity: .9; transform: scale(1); }
  35%  { opacity: 0; transform: scale(2.6); }
  100% { opacity: 0; transform: scale(2.6); }
}
/* a cursor sliding across a range (--run in user units), then snapping back */
.explainer .sweep { animation: xp-sweep var(--dur, 3s) linear calc(var(--xp-beat, 0s) + var(--delay, 0s)) infinite; }
@keyframes xp-sweep {
  0%   { transform: translateX(0); opacity: 0; }
  6%   { opacity: 1; }
  94%  { opacity: 1; }
  100% { transform: translateX(var(--run, 100px)); opacity: 0; }
}
/* rotation about an explicit point: set transform-origin inline in user units */
.explainer .spin { transform-box: view-box; animation: xp-spin var(--dur, 8s) linear calc(var(--xp-beat, 0s) + var(--delay, 0s)) infinite; }
@keyframes xp-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* slow breathing glow for "this is live" markers; --dur/--lo tune it — a slow,
   deep breathe (--dur:3.6s; --lo:.45) reads as "alive but starved" */
.explainer .breathe { animation: xp-breathe var(--dur, 1.8s) ease-in-out calc(var(--xp-beat, 0s) + var(--delay, 0s)) infinite; }
@keyframes xp-breathe { 50% { opacity: var(--lo, .35); } }

/* ---- choreography: a held step is a running system ----
   Step-clocked motion: animations inside a .cue container run only while a
   step has added `go` to it, and adding `go` starts them from t=0 — so a
   sequence (packet → landing → cell fills) always plays from step entry, and
   consecutive steps that keep `go` keep its clock running. Rest poses (the
   reduced-motion still) are the markup's inline values. */
.explainer .cue:not(.go) * { animation: none !important; }
/* gapped comet: rides its path in the first quarter of each --dur cycle, then
   stays dark — sparse, one-at-a-time traffic (pair with .flash/.ping at
   --delay + .24 × --dur to mark the arrival) */
.explainer .mv.hop { animation-name: xp-hop; animation-timing-function: linear; }
@keyframes xp-hop {
  0%   { offset-distance: 0%; opacity: 0; transform: scaleX(.7); animation-timing-function: cubic-bezier(.45,.05,.3,1); }
  3%   { opacity: 1; }
  12%  { transform: scaleX(1.35); }
  20%  { opacity: 1; }
  24%, 100% { offset-distance: 100%; opacity: 0; transform: scaleX(.75); }
}
/* .once: play a motion a single time, scheduled by --delay (e.g. the k-th
   page of a scan at k × period) */
.explainer .once { animation-iteration-count: 1; animation-iteration-count: 1 !important; }
.explainer .mv.once { animation-fill-mode: both; }
/* an arrival: the mark pops in with a small overshoot and stays (cells filling) */
.explainer .pop { animation: xp-pop .55s cubic-bezier(.3,.7,.4,1) calc(var(--xp-beat, 0s) + var(--delay, 0s)) both; }
@keyframes xp-pop { 0% { opacity: 0; transform: scale(.3); } 45% { opacity: 1; transform: scale(1.3); } 100% { opacity: 1; transform: scale(1); } }
/* activity tick: a short flash at the start of each --dur cycle (a border
   pulsing as a page passes through, a probe lighting a row) */
.explainer .flash { opacity: 0; animation: xp-flash var(--dur, 2s) ease-out calc(var(--xp-beat, 0s) + var(--delay, 0s)) infinite; }
@keyframes xp-flash { 0% { opacity: 0; } 3% { opacity: 1; } 14% { opacity: 1; } 22%, 100% { opacity: 0; } }
/* blip: visible only for a --dur window starting at --delay (give it inline
   opacity:0) — a counter stepping 1 → 2 → 3 */
.explainer .blip { animation: xp-blip var(--dur, .7s) step-end calc(var(--xp-beat, 0s) + var(--delay, 0s)) 1; }
@keyframes xp-blip { from, to { opacity: 1; } }
/* text caret / bit flicker */
.explainer .blink { animation: xp-blink var(--dur, 1.06s) step-end calc(var(--xp-beat, 0s) + var(--delay, 0s)) infinite; }
@keyframes xp-blink { 50% { opacity: 0; } }
/* waiting… — three marks lighting in turn */
.explainer .ellip > * { animation: xp-ellip 1.5s ease-in-out infinite; }
.explainer .ellip > :nth-child(2) { animation-delay: .2s; }
.explainer .ellip > :nth-child(3) { animation-delay: .4s; }
@keyframes xp-ellip { 0%, 60%, 100% { opacity: .15; } 30% { opacity: 1; } }
/* creep: a bar growing from 0 (with .grow) or a cursor (.sweep.creep) sliding
   --run, once, over --dur — progress on a slow clock. Add an inline
   animation-timing-function: steps(n, jump-start) to advance in notches. */
.explainer .creep { animation: xp-creep var(--dur, 60s) linear calc(var(--xp-beat, 0s) + var(--delay, 0s)) both; }
@keyframes xp-creep { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.explainer .sweep.creep { animation-name: xp-creep-x; }
@keyframes xp-creep-x { from { transform: translateX(0); } to { transform: translateX(var(--run, 100px)); } }
/* a cue's clock starts on the beat that switched it on: the runtime sets --xp-lag
   on the cue (the caption reveal, or the beat of the phrase whose `set` adds
   `go`). Loops normalise their phase with mod(), so a stream starts empty on the
   beat and fills into its steady state instead of being caught mid-flight;
   slow clocks (.spin, .breathe, .blink) and one-shot schedules (.once, .pop,
   .blip, .creep) keep their --delay as-is. */
.explainer .cue :is(.mv, .ping, .flash, .sweep, .spin, .breathe, .blink) { animation-delay: calc(var(--xp-lag, 0s) + var(--delay, 0s)); }
@supports (animation-delay: mod(1s, 1s)) {
  .explainer .cue :is(.mv, .ping, .flash) { animation-delay: calc(var(--xp-lag, 0s) + mod(var(--delay, 0s), var(--dur, 2s))); }
  .explainer .cue .sweep { animation-delay: calc(var(--xp-lag, 0s) + mod(var(--delay, 0s), var(--dur, 3s))); }
}
.explainer .cue :is(.mv, .sweep, .spin) { animation-fill-mode: backwards; } /* hidden / parked until their start */
.explainer .cue :is(.once, .pop, .blip, .creep) { animation-delay: calc(var(--xp-lag, 0s) + var(--delay, 0s)); }
.explainer .cue .mv.once { animation-fill-mode: both; }
.explainer .cue .ellip > * { animation-delay: var(--xp-lag, 0s); }
.explainer .cue .ellip > :nth-child(2) { animation-delay: calc(var(--xp-lag, 0s) + .2s); }
.explainer .cue .ellip > :nth-child(3) { animation-delay: calc(var(--xp-lag, 0s) + .4s); }
/* camera: wrap the scene in <g class="cam"> and give steps a transform in user
   units (translate(…px, …px) scale(s)); it dollies from rest and lands with a
   gentle spring, while entrances wait for it (the runtime delays them) */
.explainer .cam {
  transform-box: view-box; transform-origin: 0 0; transition-delay: var(--xp-beat, 0s);
  transition-duration: .45s, 1.2s, .35s, .35s, .5s;
  transition-timing-function: var(--xp-out), var(--xp-dolly), ease, ease, var(--xp-out);
}
/* off screen or in a hidden tab, ambient motion holds still (and resumes in phase) */
.explainer:not(.xp-live) > svg * { animation-play-state: paused !important; }

.xp-nomotion .mv, .xp-nomotion .sweep, .xp-nomotion .spin, .xp-nomotion .breathe { animation: none; }
.xp-nomotion .ping, .xp-nomotion .flash { animation: none; opacity: 0; }

/* legacy vocabulary (kept for older figures) */
.explainer .box { fill: none; stroke: var(--ink); stroke-width: 1.25; }
.explainer .box.dim { stroke: var(--muted); stroke-dasharray: 4 3; }
.explainer .box.hot { stroke: var(--accent); stroke-width: 1.75; }
.explainer .lbl { fill: var(--ink); font-size: 11px; letter-spacing: 1px; }
.explainer .lbl.dim { fill: var(--muted); }
.explainer .lbl.hot { fill: var(--accent); }
.explainer .note { fill: var(--muted); font-size: 10px; letter-spacing: .3px; }
.explainer .wire { stroke: var(--muted); stroke-width: 1.1; fill: none; }
.explainer .wire.hot { stroke: var(--accent); stroke-width: 1.5; }
.explainer .wire.gold { stroke: var(--gold); stroke-width: 1.5; }
