/* ============================================================================
   Orb Workday — the orb itself
   ---------------------------------------------------------------------------
   A sealed machine with something alive inside it: mass, containment, light.
   The body is a lit sphere (key light upper-left, terminator into shadow,
   specular on the surface, internal strata for depth). The SVG ring is the
   containment shell and doubles as the progress band. Every state is carried
   by structure as well as colour so it survives a greyscale screenshot.

   Requires: tokens.css (palette, easing) and motion.css (@keyframes).
   This file declares NO keyframes — it only references names owned by
   motion.css. It styles nothing outside .orb-viz.

   LOCAL TOKENS. Five colours are declared here rather than in tokens.css,
   because they are properties of LIGHT and MATERIAL, not of the interface,
   and nothing outside this component has any use for them:
     --_incandescent  the colour of the light itself. Warm white in BOTH
                      themes — never --fg, which inverts.
     --_ambient       the cool light the room returns. Biases the shadow side.
     --_spec          the specular. Hotter than --glint, which is an interface
                      sheen and dissolves on a 260px sphere.
     --_frost         rime deposit (paused).
     --_cleft /
     --_cleft-lip     the dark and lit halves of a fracture (failed).
   Four of them flip in section 9 (--_frost, --_cleft, --_cleft-lip, and the
   derived --_rim), because a deposit, a crack and a rim all have to invert
   against the ground they sit on. Three are theme-STABLE by design:
   --_incandescent and --_spec are the colour of light, which does not change
   because the page did, and --_ambient is the cool bias, which reads correctly
   at both ends. That stability is the fix for the old light-theme bug, not an
   oversight.

   ---------------------------------------------------------------------------
   PUBLIC CUSTOM PROPERTY API  (set these from JS / inline style)
   ---------------------------------------------------------------------------
   --orb-px        <number>   default 88     Diameter in CSS pixels, UNITLESS
                                             (e.g. style="--orb-px: 220").
                                             Registered, so a bad value falls
                                             back to 88 instead of collapsing.
   --orb-size      <length>   unset          Optional escape hatch: any length
                                             (10rem, 40px, 100%…). Wins over
                                             --orb-px when present.
   --orb-progress  <number>   0              0–1. Drives the ring arc, and the
                                             hue-independent length of the
                                             progress band. Values outside the
                                             range are clamped.
   --orb-ping-ms   <time>     900ms          Duration of the one-shot shockwave
                                             fired by adding class .is-pinging.

   ---------------------------------------------------------------------------
   ATTRIBUTE API
   ---------------------------------------------------------------------------
   [data-status]   provisioning | setup | ready | working | resuming |
                   paused | failed          (default: ready)
   [data-size]     a1.tiny | a1.small | a1.medium | a1.large | a1.xxlarge
                   Changes internal density, ring weight and apparent mass —
                   never the diameter. Diameter is the lead's business.
   .is-pinging     Add for ~900ms to fire the shockwave. Remove to re-arm.

   ---------------------------------------------------------------------------
   RING MATH  (auditable)
   ---------------------------------------------------------------------------
   viewBox 0 0 120 120, circle r = 54.
   circumference C = 2 * PI * 54 = 339.29200658769764  ->  339.292 used below.
   Progress p: stroke-dasharray: C; stroke-dashoffset: C * (1 - p).
   The arc is rotated -90deg about the circle's own box so 0 starts at 12
   o'clock (transform-box: fill-box is required or it rotates about the SVG
   origin and swings off-canvas).
   ========================================================================= */

/* --- Registered properties ------------------------------------------------
   Registration buys three things: type safety (a wrong unit degrades to the
   initial value rather than invalidating the whole declaration), inheritance
   we control, and interpolatable custom properties. */

@property --orb-px {
  syntax: "<number>";
  inherits: true;
  initial-value: 88;
}
@property --orb-progress {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

/* ==========================================================================
   1. Component box
   ========================================================================== */

.orb-viz {
  /* Resolved diameter. --orb-size (a length) wins; otherwise --orb-px * 1px. */
  --_d: var(--orb-size, calc(var(--orb-px, 88) * 1px));

  /* Progress, clamped, as a plain number. */
  --_p: clamp(0, var(--orb-progress, 0), 1);

  /* --- Fleet phase ---------------------------------------------------------
     Every looping animation on this orb starts at a negative delay derived
     from the orb's OWN progress and diameter, so no two orbs in a fleet ever
     breathe in step. A grid of parallel machines then reads as a system that
     came alive at different moments rather than as N copies of one loop.

     This uses only values already in the public API — it needs no new
     property, and the lead's JS does not have to know it exists. Progress is
     the right carrier because it is the one number that is genuinely
     different per orb at the moment a fleet is running.

     The magnitude is deliberately small — 1.9s of phase across the whole 0–1
     range — because a live progress update re-maps a running animation's
     phase. Measured: under a requestAnimationFrame ramp the loops stay
     perfectly smooth (step-to-step variation under 1e-4 of a cycle); the only
     effect is that a working orb's breathing runs 1.9s of extra phase over the
     span of an entire task, which on any realistic task length is a
     single-digit percentage and is not perceivable as a change of speed. */
  --_phase: calc(var(--_p) * -1.9s + var(--orb-px, 88) * -0.007s);

  /* Ring geometry, in viewBox units.
     Division of labour, so [data-status] and [data-size] (equal specificity)
     can never fight: STATE owns the base weights and every visual knob below;
     SIZE owns only the multipliers (--_ring-k, --_core-k), the strata count
     (--_bands) and the track weight. Nothing is set by both. */
  --_c: 339.292;
  --_ring-w: 2.4;          /* arc weight, set per state */
  --_ring-k: 1;            /* arc weight multiplier, set per size */
  --_track-w: 1.2;
  /* dash + gap must BOTH be a full circumference for progress to read: with a
     zero gap the pattern is continuous and stroke-dashoffset does nothing. */
  --_arc-dash: var(--_c);
  --_arc-gap: var(--_c);

  /* --- Local tokens (NOT in tokens.css — declared here, owned here) --------
     Three anchors the sphere's whole material is built from. They are colours
     of LIGHT, not colours of interface, which is why they do not belong in the
     shared palette:

       --_incandescent  the colour of the light itself at its hottest. A warm
                        white in both themes. Deliberately NOT --fg: --fg
                        inverts to near-black in the light theme, which is what
                        turned the core into a dark smudge on a pale page.
       --_ambient       the cool light the room throws back. Used only to bias
                        the shadow side a few percent cooler than the lit side.
                        This chromatic split — warm core, cool limb — is what
                        makes the sphere read as lit rather than as tinted.
       --_bloom-k       how far the internal light bleeds through the shell.
                        Scaled per state; 0 is a dead object. */
  --_incandescent: #fffdf4;
  --_ambient: #8ea3c4;
  --_bloom-k: 1;
  --_bloom-s: 1;         /* size scalar for the same, set in section 10 */

  /* State colour + the modelling knobs derived from it. Defaults = ready. */
  --_state: var(--state-ready);
  --_tint: 58%;            /* how much state colour survives in the lit area */
  --_body-op: 1;           /* whole-sphere presence */
  --_core-op: 0.5;         /* the thing alive inside */
  --_core-size: 52%;       /* set per state */
  --_core-k: 1;            /* multiplier, set per size */
  --_halo-op: 0.5;
  --_crystal: 0;           /* frost facets */
  --_fracture: 0;          /* crack across the body */
  --_bands-op: 0.5;
  --_bands: 5;             /* strata count; [data-size] retunes this */
  --_shade: var(--inset);  /* terminator ink */

  /* Derived sphere palette. oklab keeps mid-tones from going muddy.
     Three families, and the split between them is the whole material:
       hot / glow   the light INSIDE, resolving towards --_incandescent
       lit / mid    the shell where the key light lands
       dark / deep  the shell in shadow, biased towards --_ambient
     Nothing here reaches for --fg on the light side any more. */
  --_hot:  color-mix(in oklab, var(--_state) 22%, var(--_incandescent));
  --_glow: color-mix(in oklab, var(--_state) 82%, var(--_incandescent));
  --_lit:  color-mix(in oklab, var(--_state) var(--_tint), var(--_incandescent));
  --_mid:  color-mix(in oklab, var(--_state) 72%, var(--bg-sunken));
  --_dark: color-mix(in oklab, var(--_state) 26%,
             color-mix(in oklab, var(--_ambient) 8%, var(--bg-sunken)));
  --_deep: color-mix(in oklab, var(--_state) 9%,
             color-mix(in oklab, var(--_ambient) 12%, var(--bg-sunken)));
  --_edge: color-mix(in oklab, var(--_state) 40%, transparent);
  /* The rim: light wrapping the shaded limb, which is what lifts the sphere
     off the ground plane. Its hue is the object's own light, not the page's. */
  --_rim: color-mix(in oklab, var(--_glow) 46%, transparent);
  /* Frost ink, and the two halves of a fracture. Named because both have to
     invert with the theme: a cleft is always DARKER than the shell it splits,
     and its lit lip is always BRIGHTER, whichever way the page runs. */
  /* The specular. --glint alone is a 12%-alpha white built for interface
     surfaces; on a 260px sphere it dissolves and the object goes airbrushed.
     A lit dielectric has ONE small, hot, hard-edged kick — that single detail
     is most of what separates a rendered sphere from a coloured circle. */
  --_spec:      color-mix(in oklab, var(--_incandescent) 66%, transparent);
  --_frost:     color-mix(in oklab, var(--_ambient) 22%, var(--fg));
  --_cleft:     color-mix(in oklab, var(--bg-sunken) 92%, transparent);
  --_cleft-lip: color-mix(in oklab, var(--fg) 52%, transparent);

  position: relative;
  display: inline-block;
  flex: none;
  width: var(--_d);
  height: var(--_d);
  /* Everything inside is sized in % or em of this, so the whole component is
     a pure function of --_d. em is the small-detail unit: 1em == 0.1 * d. */
  font-size: calc(var(--_d) / 10);
  line-height: 0;
  vertical-align: middle;
  isolation: isolate;
  color: var(--_state);
  /* inline-size containment gives the component its own query context, so the
     small/large refinements in section 10 key off the orb's own diameter and
     not the viewport. It also contains layout and style. */
  container-type: inline-size;
}

/* --- The knurled collar ---------------------------------------------------
   A band of fine machined ticks sitting in the gap between the sphere and the
   containment ring. This is what stops the ring reading as a loading spinner:
   the arc is running around a milled collar, not floating in space. Tick
   density is one of the ways [data-size] shows apparent mass.
   ::before on the component root — the child markup contract is untouched. */

.orb-viz::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  opacity: calc(var(--_bezel-op, 0.5) * var(--_bezel-k, 1));
  background: repeating-conic-gradient(from 0deg at 50% 50%,
    color-mix(in oklab, var(--fg) 38%, transparent) 0deg calc(360deg / var(--_ticks, 36) * 0.26),
    transparent calc(360deg / var(--_ticks, 36) * 0.26) calc(360deg / var(--_ticks, 36)));
  -webkit-mask-image: radial-gradient(closest-side circle,
    transparent 0 79%, black 82% 87%, transparent 90%);
  mask-image: radial-gradient(closest-side circle,
    transparent 0 79%, black 82% 87%, transparent 90%);
  transition: opacity var(--cue-d, 130ms) var(--ease-out);
}

/* ==========================================================================
   2. Halo — contained bloom, not a blur-glow div
   The light does not leak: it sits in a narrow band just off the sphere's
   limb, the way light wraps a lit object, and stops.
   ========================================================================== */

.orb-viz__halo {
  position: absolute;
  /* Deliberately larger than the component box. Light in air does not stop at
     a layout boundary, and confining the bloom to inset:0 left it only 11% of
     the diameter to fall off in — which is why it drew a ring instead of an
     atmosphere. 9% is the most it can overhang before a tight fleet grid
     starts to bleed; the alpha at that radius is under 4%, so it reads as air.
     .orb-viz sets container-type, which contains layout and style but NOT
     paint, so this is allowed to overflow. */
  inset: -9%;
  border-radius: 50%;
  opacity: var(--_halo-op);
  /* The sphere's limb lands at ~56% of this box's closest-side radius, so the
     bloom starts just inside the shell and falls away across the whole of the
     rest of it. No stop is steep enough anywhere to draw an edge. The hue is
     --_glow, not --_state: what leaves the object is the colour of its light. */
  background:
    radial-gradient(closest-side circle at 50% 50%,
      transparent 0 50%,
      color-mix(in oklab, var(--_glow) 14%, transparent) 58%,
      color-mix(in oklab, var(--_glow) 7%, transparent) 71%,
      color-mix(in oklab, var(--_state) 3%, transparent) 86%,
      transparent 100%);
  transition: opacity var(--cue-d, 130ms) var(--ease-out);
  animation-name: var(--motion-off, orb-halo-swell);
  animation-duration: 7.2s;
  animation-delay: var(--_phase);
  animation-timing-function: var(--ease-in-out);
  animation-iteration-count: infinite;
  animation-play-state: paused;   /* states opt in */
}

/* ==========================================================================
   3. Body — the sphere
   ========================================================================== */

.orb-viz__body {
  position: absolute;
  inset: 11%;
  border-radius: 50%;
  overflow: hidden;
  opacity: var(--_body-op);
  background:
    /* SUBSURFACE — the light inside, seen through the shell. Painted over the
       form gradient rather than under it, because that is what light escaping
       a translucent mantle actually does to the value beneath it. Its radius
       is far wider than the core disc, so the shell is warmed well outside the
       core's own edge: the mantle glows, the core does not float. */
    radial-gradient(circle at 50% 48%,
      color-mix(in oklab, var(--_glow) calc(24% * var(--_bloom-k) * var(--_bloom-s)), transparent) 0%,
      color-mix(in oklab, var(--_glow) calc(11% * var(--_bloom-k) * var(--_bloom-s)), transparent) 34%,
      color-mix(in oklab, var(--_state) calc(4% * var(--_bloom-k) * var(--_bloom-s)), transparent) 62%,
      transparent 86%),
    /* FORM — key light upper-left, terminator into a cooler shadow. */
    radial-gradient(circle at 31% 25%,
      var(--_lit)  0%,
      var(--_mid)  30%,
      var(--_dark) 63%,
      var(--_deep) 100%);
  /* Four shadows, in paint order: the machined lip at the limb; the rim light
     (an inset shadow offset up-left, so the sliver it leaves falls on the
     lower-right limb — exactly opposite the key); the terminator ink; and the
     seat underneath. Small, cheap, no large spreads. */
  box-shadow:
    inset 0 0 0 0.06em var(--_edge),
    inset -0.05em -0.06em 0.1em -0.02em var(--_rim),
    inset -0.18em -0.24em 0.5em var(--_shade),
    0 0.22em 0.5em -0.16em var(--_shade);
  transition:
    opacity var(--cue-d, 130ms) var(--ease-out),
    background-color var(--cue-d, 130ms) var(--ease-out),
    transform var(--tween-d, 280ms) var(--ease-orb);
  animation-name: var(--motion-off, orb-breathe);
  animation-duration: 5.6s;
  animation-delay: var(--_phase);
  animation-timing-function: var(--ease-in-out);
  animation-iteration-count: infinite;
  animation-play-state: paused;
}

/* --- Core: the thing alive inside ---------------------------------------- */

.orb-viz__core {
  --_cs: calc(var(--_core-size) * var(--_core-k, 1));
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--_cs);
  height: var(--_cs);
  margin: calc(var(--_cs) / -2) 0 0 calc(var(--_cs) / -2);
  border-radius: 50%;
  opacity: var(--_core-op);
  /* Four zones, and the hue walks across them: incandescent at the filament,
     the state's own colour through the mantle, and the state at low alpha at
     the edge so there is no drawn boundary anywhere. The hottest stop resolves
     towards --_incandescent and NOT --fg — that is the difference between a
     light source and, on a pale page, a dark bruise. */
  background:
    radial-gradient(closest-side circle at 45% 41%,
      var(--_hot) 0%,
      var(--_glow) 22%,
      color-mix(in oklab, var(--_state) 84%, transparent) 46%,
      color-mix(in oklab, var(--_state) 30%, transparent) 72%,
      transparent 100%);
  /* The bleed. Clipped by .orb-viz__body's overflow, so this is light stopped
     by the shell rather than a glow escaping into the page. */
  box-shadow: 0 0 1.1em -0.1em
    color-mix(in oklab, var(--_glow) calc(30% * var(--_bloom-k) * var(--_bloom-s)), transparent);
  transition:
    opacity var(--cue-d, 130ms) var(--ease-out),
    width var(--tween-d, 280ms) var(--ease-orb),
    height var(--tween-d, 280ms) var(--ease-orb);
  animation-name: var(--motion-off, orb-core-pulse);
  animation-duration: 4.4s;
  animation-delay: var(--_phase);
  animation-timing-function: var(--ease-in-out);
  animation-iteration-count: infinite;
  animation-play-state: paused;
}

/* --- Bands: internal strata, in perspective ------------------------------
   Horizontal planes tilted away from the viewer. This is what stops the body
   reading as a flat disc: the strata converge towards the equator and are
   masked out before they reach the limb, so the eye reads volume. */

.orb-viz__bands {
  position: absolute;
  left: -25%;
  right: -25%;
  top: -60%;
  bottom: -60%;
  opacity: calc(var(--_bands-op) * var(--_bands-k, 1));
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent calc(50% / var(--_bands) - 0.05em),
    color-mix(in oklab, var(--_glow) 52%, transparent) calc(50% / var(--_bands) - 0.05em),
    color-mix(in oklab, var(--_glow) 52%, transparent) calc(50% / var(--_bands)),
    transparent calc(50% / var(--_bands))
  );
  transform: perspective(6em) rotateX(58deg) scale(1.05);
  transform-origin: 50% 46%;
  -webkit-mask-image: radial-gradient(
    ellipse 27% 18% at 50% 49%,
    black 0%, black 22%, transparent 78%);
  mask-image: radial-gradient(
    ellipse 27% 18% at 50% 49%,
    black 0%, black 22%, transparent 78%);
  transition: opacity var(--cue-d, 130ms) var(--ease-out);
  animation-name: var(--motion-off, orb-bands-travel);
  animation-duration: 2.6s;
  animation-delay: var(--_phase);
  animation-timing-function: linear; /* a travelling belt genuinely is linear */
  animation-iteration-count: infinite;
  animation-play-state: paused;
}

/* --- Specular: a highlight that sits ON the surface -----------------------
   Two parts: a soft wrap of reflected sky, and a tight kick where the key
   light hits. Both are elliptical and rotated to follow the sphere's curve. */

.orb-viz__specular {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* NOTE ON SYNTAX, because this layer was silently dead before:
     radial-gradient() takes an extent KEYWORD (closest-side, farthest-corner…)
     OR an explicit size, never both. `closest-side ellipse 15% 11%` is
     invalid, and one invalid gradient invalidates the whole `background`
     shorthand — every layer here was being dropped, which is why the sphere
     had no highlight at any size. Explicit sizes only, no keyword. */
  background:
    /* the kick: small, hot, and with a fast falloff so it holds an edge */
    radial-gradient(ellipse 9% 6.6% at 33% 26%,
      var(--_spec) 0%,
      color-mix(in oklab, var(--_spec) 60%, transparent) 40%,
      color-mix(in oklab, var(--_spec) 13%, transparent) 74%,
      transparent 100%),
    /* the sheen: a wider, much weaker pool around the kick — the polish on the
       shell, distinct from the kick itself */
    radial-gradient(ellipse 23% 18% at 34% 28%,
      color-mix(in oklab, var(--_spec) 18%, transparent) 0%,
      transparent 100%),
    /* soft wrap along the lit limb */
    radial-gradient(ellipse 44% 38% at 36% 30%,
      color-mix(in oklab, var(--glint) 40%, transparent) 0%,
      transparent 100%),
    /* bounce light picked up from below-right, keeps the shadow side alive */
    radial-gradient(ellipse 42% 33% at 71% 78%,
      color-mix(in oklab, var(--_state) 26%, transparent) 0%,
      transparent 100%);
  transform: rotate(-12deg);
  transition: opacity var(--cue-d, 130ms) var(--ease-out);
}

/* --- Terminator: the falloff into shadow, plus the frost / fracture stage - */

.orb-viz__terminator {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle at 27% 20%,
      transparent 34%,
      color-mix(in oklab, var(--_shade) 55%, transparent) 74%,
      var(--_shade) 100%);
  transition: opacity var(--cue-d, 130ms) var(--ease-out);
}

/* Frost facets (paused) and fracture (failed) share this layer's ::before /
   ::after so no extra markup is needed. Both are off by default. */

.orb-viz__terminator::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  opacity: var(--_crystal);
  /* Rime. Two spur sets at different angular pitches and — critically — with
     different reach, so the crystals are irregular needles growing in from the
     limb rather than a regular pinwheel. The old single set reached to 34% of
     the radius, which at 260px drew a spiderweb across the whole sphere. Both
     sets are now confined to the outer third by the element's own mask. */
  background:
    /* long needles, sparse */
    repeating-conic-gradient(from 7deg at 50% 50%,
      transparent 0deg 9deg,
      color-mix(in oklab, var(--_frost) 30%, transparent) 9deg 10.4deg,
      transparent 10.4deg 21deg),
    /* short needles, dense, off-phase — breaks the regularity of the first */
    repeating-conic-gradient(from 3.4deg at 50% 50%,
      transparent 0deg 5.6deg,
      color-mix(in oklab, var(--_frost) 20%, transparent) 5.6deg 6.5deg,
      transparent 6.5deg 13.7deg),
    /* fine hoar texture, off-axis so it does not line up with the spurs */
    repeating-linear-gradient(58deg,
      transparent 0 0.14em,
      color-mix(in oklab, var(--_frost) 11%, transparent) 0.14em 0.2em,
      transparent 0.2em 0.42em),
    /* the frozen rim itself */
    radial-gradient(closest-side circle at 50% 50%,
      transparent 0 62%,
      color-mix(in oklab, var(--_frost) 16%, transparent) 88%,
      color-mix(in oklab, var(--_frost) 30%, transparent) 100%);
  -webkit-mask-image: radial-gradient(closest-side circle,
    transparent 0 56%, black 78%, black 92%, transparent 100%);
  mask-image: radial-gradient(closest-side circle,
    transparent 0 56%, black 78%, black 92%, transparent 100%);
  transition: opacity var(--cue-d, 130ms) var(--ease-out);
}

.orb-viz__terminator::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  opacity: var(--_fracture);
  /* Two fissures, each a dark cleft with a lit lip on one side, so the crack
     reads as depth rather than as a drawn line. */
  background-image:
    /* main fissure, running high-left to low-right */
    linear-gradient(107deg,
      transparent 0 43.2%,
      /* penumbra: the shell dips towards the cleft before it breaks. Without
         this the fissure is a drawn line; with it, it is a valley. */
      color-mix(in oklab, var(--_cleft) 34%, transparent) 43.2% 44.4%,
      var(--_cleft) 44.4% 46.6%,
      var(--_cleft-lip) 46.6% 47.5%,
      color-mix(in oklab, var(--_cleft-lip) 22%, transparent) 47.5% 48.2%,
      transparent 48.6% 100%),
    /* branch splitting off the main fissure and running down-left, so the two
       read as one break propagating rather than as a drawn cross */
    linear-gradient(152deg,
      transparent 0 55.5%,
      color-mix(in oklab, var(--_cleft) 84%, transparent) 55.5% 57.1%,
      color-mix(in oklab, var(--_cleft-lip) 62%, transparent) 57.1% 57.8%,
      transparent 57.8% 100%);
  /* the branch and the cleft only exist where the shell actually gave way —
     fading them out at the limb stops the crack reading as a drawn X */
  -webkit-mask-image: radial-gradient(closest-side circle, black 0 52%, transparent 96%);
  mask-image: radial-gradient(closest-side circle, black 0 52%, transparent 96%);
  transition: opacity var(--cue-d, 130ms) var(--ease-out);
}

/* ==========================================================================
   4. Containment ring — the shell, and the progress band
   ========================================================================== */

.orb-viz__ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  display: block;
  pointer-events: none;
}

.orb-viz__ring-track,
.orb-viz__ring-arc {
  fill: none;
  transform-box: fill-box;
  transform-origin: 50% 50%;
  stroke-linecap: butt;   /* a machined band has square ends, not lozenges */
}

.orb-viz__ring-track {
  stroke: var(--line-strong);
  stroke-width: var(--_track-w);
  transition: stroke var(--cue-d, 130ms) var(--ease-out),
              stroke-width var(--cue-d, 130ms) var(--ease-out);
}

.orb-viz__ring-arc {
  stroke: var(--_state);
  stroke-width: calc(var(--_ring-w) * var(--_ring-k, 1));
  /* C = 339.292 (r=54). Solid band by default; states override --_arc-dash. */
  stroke-dasharray: var(--_arc-dash) var(--_arc-gap);
  stroke-dashoffset: calc(var(--_c) * (1 - var(--_p)));
  transform: rotate(-90deg);
  transition:
    stroke-dashoffset var(--tween-slow-d, 520ms) var(--ease-orb),
    stroke var(--cue-d, 130ms) var(--ease-out),
    stroke-width var(--cue-d, 130ms) var(--ease-out),
    opacity var(--cue-d, 130ms) var(--ease-out);
  animation-name: var(--motion-off, none);
  animation-duration: 2.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* ==========================================================================
   5. Ping — a shockwave with weight
   Two fronts: a hard leading edge that snaps outward and decelerates, and a
   slower pressure wave behind it. ::after is an opacity-only flash that is
   deliberately NOT gated on motion, so the ping is still felt under reduced
   motion without anything moving.
   ========================================================================== */

.orb-viz__ping {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  border: 0.14em solid color-mix(in oklab, var(--_state) 80%, transparent);
  box-shadow: 0 0 0 0.04em color-mix(in oklab, var(--_state) 30%, transparent);
}

.orb-viz__ping::after {
  content: "";
  position: absolute;
  inset: -0.14em;
  border-radius: 50%;
  opacity: 0;
  background: radial-gradient(closest-side circle,
    color-mix(in oklab, var(--_state) 26%, transparent) 0 62%,
    transparent 100%);
}

.orb-viz.is-pinging .orb-viz__ping {
  animation-name: var(--motion-off, orb-ping-wave);
  animation-duration: var(--orb-ping-ms, 900ms);
  animation-timing-function: var(--ease-orb);
  animation-iteration-count: 1;
}

.orb-viz.is-pinging .orb-viz__ping::after {
  /* Two animations on one element, gated independently. The pressure front
     moves and is therefore gated on --motion-off; the flash is 220ms of light
     with zero movement and is NOT gated, so a completed task is never silent
     under reduced motion. */
  animation-name: orb-ping-flash, var(--motion-off, orb-ping-pressure);
  animation-duration: 220ms, var(--orb-ping-ms, 900ms);
  animation-timing-function: var(--ease-out), var(--ease-orb);
  animation-iteration-count: 1, 1;
}

/* ==========================================================================
   6. Glyph — the greyscale-safe state mark
   A small engraved badge riding the ring at the lower right. Masked inline
   SVG (data URI, no network) painted with currentColor, so it is always the
   state colour on a plate of page ground with a hairline rim: legible in
   both themes, and legible with all colour removed because the SHAPE differs
   per state.
   ========================================================================== */

.orb-viz__glyph {
  /* --_badge is the ONE knob. It is retuned per container size in section 10,
     because a badge that is a constant fraction of the diameter is correct at
     40px and absurd at 260px: it stops being part of the object and becomes a
     button parked on top of it. */
  --_badge: 2.9em;

  position: absolute;
  /* Seated ON the containment ring at 4:30, not hung off the corner. The ring
     is r=54 of a 120 viewBox = 45% of the diameter; 45% * cos(45deg) = 31.8%,
     so 50% + 31.8% = 81.8% puts the badge's centre exactly on the band. The
     badge therefore reads as a boss machined into the ring — the ring runs
     behind it and comes out the other side — rather than as a floating chip. */
  left: 81.8%;
  top: 81.8%;
  width: var(--_badge);
  height: var(--_badge);
  margin: calc(var(--_badge) / -2) 0 0 calc(var(--_badge) / -2);
  border-radius: 50%;
  /* A plate of page ground with a lit top edge, so it is a milled part with a
     highlight rather than a flat swatch. The ground keeps glyph contrast
     identical to the surrounding text, which is what the greyscale and AA
     requirements rest on. */
  background:
    linear-gradient(196deg,
      color-mix(in oklab, var(--_glow) 10%, var(--bg)) 0%,
      var(--bg) 46%,
      color-mix(in oklab, var(--bg-sunken) 60%, var(--bg)) 100%);
  box-shadow:
    inset 0 0 0 0.1em color-mix(in oklab, var(--_state) 62%, var(--line-strong)),
    inset 0 0.06em 0.1em -0.04em color-mix(in oklab, var(--_glow) 30%, transparent),
    0 0.08em 0.22em -0.02em var(--_shade);
  transition: box-shadow var(--cue-d, 130ms) var(--ease-out),
              background-color var(--cue-d, 130ms) var(--ease-out);
}

.orb-viz__glyph::before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: currentColor;
  -webkit-mask-position: center;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: 58% 58%;
  -webkit-mask-image: var(--_glyph, none);
  mask-position: center;
  mask-repeat: no-repeat;
  mask-size: 58% 58%;
  mask-image: var(--_glyph, none);
  transition: opacity var(--cue-d, 130ms) var(--ease-out);
}

/* ==========================================================================
   7. The seven states
   Each block sets: the colour token, the sphere modelling knobs, the ring
   treatment, the glyph shape, and which loops are running. Colour is the
   last thing each state relies on — remove it and ring pattern + glyph shape
   still separate all seven.
   ========================================================================== */

/* --- provisioning: the shell is being drawn -------------------------------
   Structure: ring is a short arc sweeping round a nearly-empty track; body is
   faint and unformed; glyph is a broken outline circle. */
.orb-viz[data-status="provisioning"] {
  --_bezel-k: 0.3;      /* the collar is not finished being milled yet */
  --_state: var(--state-provisioning);
  --_bloom-k: 0.34;   /* the light is not up yet */
  --_tint: 52%;
  --_body-op: 0.55;
  --_core-op: 0.22;
  --_core-size: 34%;
  --_halo-op: 0.25;
  --_bands-op: 0.22;
  --_ring-w: 2;
  --_arc-dash: 62;
  --_arc-gap: 277.3;   /* 339.292 - 62 */
  --_glyph:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round'%3E%3Cpath d='M12 3.4a8.6 8.6 0 0 1 7.4 4.3'/%3E%3Cpath d='M20.4 12.9a8.6 8.6 0 0 1-4.6 6.9'/%3E%3Cpath d='M11 20.5A8.6 8.6 0 0 1 4 14.6'/%3E%3Cpath d='M4.6 8.4A8.6 8.6 0 0 1 8 4.6'/%3E%3C/g%3E%3C/svg%3E");
}
.orb-viz[data-status="provisioning"] .orb-viz__ring-arc {
  /* offset 0 so that with motion off the 62-unit arc parks at 12 o'clock
     instead of wherever stored progress would have put it */
  stroke-dashoffset: 0;
  animation-name: var(--motion-off, orb-ring-draw);
  animation-duration: 2.2s;
  animation-timing-function: var(--ease-in-out);
}
.orb-viz[data-status="provisioning"] .orb-viz__ring-track {
  stroke-dasharray: 1.6 5.4;   /* the track itself is still only pencilled in */
}
.orb-viz[data-status="provisioning"] .orb-viz__specular { opacity: 0.4; }

/* --- setup: machinery running inside --------------------------------------
   Structure: ring is a dashed band travelling clockwise; internal strata are
   bright and moving; warm. Glyph is a stack of script lines. */
.orb-viz[data-status="setup"] {
  --_state: var(--state-setup);
  --_bloom-k: 0.85;
  --_tint: 52%;
  --_core-op: 0.5;
  --_core-size: 46%;
  --_halo-op: 0.45;
  --_bands-op: 0.9;
  --_ring-w: 2.6;
  --_arc-dash: 9;
  --_arc-gap: 7;
  --_glyph:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='black'%3E%3Crect x='3' y='4.6' width='18' height='2.8' rx='1.4'/%3E%3Crect x='3' y='10.6' width='12.5' height='2.8' rx='1.4'/%3E%3Crect x='3' y='16.6' width='16' height='2.8' rx='1.4'/%3E%3C/g%3E%3C/svg%3E");
}
.orb-viz[data-status="setup"] .orb-viz__bands {
  animation-play-state: running;
}
.orb-viz[data-status="setup"] .orb-viz__ring-arc {
  stroke-dashoffset: 0;
  animation-name: var(--motion-off, orb-ring-travel);
}

/* --- ready: booted, lit, waiting ------------------------------------------
   Structure: complete unbroken hairline ring, quiet body, nothing moving.
   Glyph is a ring with a dot at its centre: at rest, holding. */
.orb-viz[data-status="ready"] {
  --_state: var(--state-ready);
  --_bloom-k: 0.5;    /* powered, idling — a pilot light, not a furnace */
  --_tint: 72%;
  --_core-op: 0.42;
  --_core-size: 46%;
  --_halo-op: 0.3;
  --_bands-op: 0.42;
  --_ring-w: 2;
  --_glyph:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='8.4' fill='none' stroke='black' stroke-width='2.2'/%3E%3Ccircle cx='12' cy='12' r='3' fill='black'/%3E%3C/svg%3E");
}
.orb-viz[data-status="ready"] {
  /* Lit but not working: the sphere is clearly powered — you can see the
     whole of it — it is simply doing nothing. Cool, even, no hot centre. */
  --_lit:  color-mix(in oklab, var(--_state) 40%, var(--fg));
  --_mid:  color-mix(in oklab, var(--_state) 92%, var(--bg-sunken));
  --_dark: color-mix(in oklab, var(--_state) 42%, var(--bg-sunken));
  --_deep: color-mix(in oklab, var(--_state) 16%, var(--bg-sunken));
}
.orb-viz[data-status="ready"] .orb-viz__ring-arc {
  /* Ready shows its shell complete regardless of stored progress. */
  stroke-dashoffset: 0;
  opacity: 0.75;
}

/* --- working: something alive inside --------------------------------------
   Structure: brightest body, core breathing, halo swelling, and the ring arc
   is a real progress band — the only state where the arc length means
   something. Glyph is a solid play triangle. */
.orb-viz[data-status="working"] {
  --_bezel-k: 1.15;
  --_state: var(--state-working);
  --_bloom-k: 1.4;    /* the showpiece: the mantle is genuinely lit through */
  --_tint: 62%;
  --_core-op: 0.9;
  --_core-size: 58%;
  --_halo-op: 0.72;
  --_bands-op: 0.55;
  --_ring-w: 3.4;
  --_glyph:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.4 4.8 19 12 8.4 19.2Z' fill='black'/%3E%3C/svg%3E");
}
.orb-viz[data-status="working"] .orb-viz__body,
.orb-viz[data-status="working"] .orb-viz__core,
.orb-viz[data-status="working"] .orb-viz__halo {
  animation-play-state: running;
}

/* --- resuming: waking -----------------------------------------------------
   Structure: ring snaps back to solid and is the heaviest of any state; a
   quick expanding pulse runs through the body; glyph is a double chevron
   climbing. Momentary by design — the lead holds it ~1s. */
.orb-viz[data-status="resuming"] {
  --_state: var(--state-resuming);
  --_bloom-k: 1.15;
  --_tint: 58%;
  --_core-op: 0.72;
  --_core-size: 54%;
  --_halo-op: 0.32;   /* resuming's token is the palest of the seven, so the
                         same numeric bloom reads twice as loud here */
  --_bands-op: 0.5;
  --_ring-w: 4.2;
  --_glyph:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-width='2.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5.5 12.6 6.5-6.4 6.5 6.4'/%3E%3Cpath d='m5.5 19.2 6.5-6.4 6.5 6.4'/%3E%3C/g%3E%3C/svg%3E");
}
.orb-viz[data-status="resuming"] .orb-viz__ring-arc {
  stroke-dashoffset: 0;
  animation-name: var(--motion-off, orb-ring-snap);
  animation-duration: 900ms;
  animation-timing-function: var(--ease-out);
  animation-iteration-count: infinite;
}
.orb-viz[data-status="resuming"] .orb-viz__body {
  animation-name: var(--motion-off, orb-wake-pulse);
  /* MUST reset the delay. The base .orb-viz__body rule carries the fleet phase
     (a negative --_phase), which is correct for the breathing LOOP it declares
     there — but this state replaces that loop with a ONE-SHOT. A one-shot
     started at a negative delay begins part-way through itself, so the wake
     would skip its own launch and start from an arbitrary point in the curve.
     Phase-stagger a fleet's loops; never a fleet's events. */
  animation-delay: 0s;
  /* Longer than it was: the four-stop curve needs time for the second bounce
     to be felt rather than merely rendered. The curve is --ease-orb, the house
     heavy-launch-long-settle, not --ease-out — this is mass moving. */
  animation-duration: 1.9s;
  animation-timing-function: var(--ease-orb);
  animation-play-state: running;
}

/* --- paused: dormant, costing nothing -------------------------------------
   Structure: crystalline frost across the body, cooled and desaturated, the
   specular gone matte, ring thin and dashed all the way round. Reads asleep,
   not broken: the frost is orderly, the ring is intact, nothing is missing.
   Glyph is the pause bar pair. */
.orb-viz[data-status="paused"] {
  --_bezel-k: 0.55;     /* rimed over */
  --_state: var(--state-paused);
  --_bloom-k: 0.05;   /* out. Only the room lights it now. */
  --_tint: 22%;
  --_body-op: 0.82;
  --_core-op: 0.16;
  --_core-size: 38%;
  --_halo-op: 0.1;
  --_bands-op: 0.14;
  --_crystal: 0.85;
  --_ring-w: 1.4;
  --_arc-dash: 3.4;
  --_arc-gap: 5.6;
  --_glyph:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='black'%3E%3Crect x='5.6' y='4.4' width='4.4' height='15.2' rx='1.4'/%3E%3Crect x='14' y='4.4' width='4.4' height='15.2' rx='1.4'/%3E%3C/g%3E%3C/svg%3E");
}
.orb-viz[data-status="paused"] {
  /* Cooled, not extinguished. The lit side keeps enough value that the sphere
     is still a sphere; it has simply lost its own light and is now only
     catching the room's. */
  --_lit:  color-mix(in oklab, var(--_state) 62%, var(--fg-faint));
  --_mid:  color-mix(in oklab, var(--_state) 74%, var(--bg-sunken));
  --_dark: color-mix(in oklab, var(--_state) 30%, var(--bg-sunken));
  --_deep: color-mix(in oklab, var(--_state) 12%, var(--bg-sunken));
  --_edge: color-mix(in oklab, var(--fg) 24%, transparent);
}
.orb-viz[data-status="paused"] .orb-viz__ring-arc {
  stroke-dashoffset: 0;
  opacity: 0.85;
}
.orb-viz[data-status="paused"] .orb-viz__specular { opacity: 0.22; }
.orb-viz[data-status="paused"] .orb-viz__terminator::before {
  animation-name: var(--motion-off, orb-frost-settle);
  animation-duration: 1.1s;
  animation-timing-function: var(--ease-out);
  animation-iteration-count: 1;
}

/* --- failed: something went wrong -----------------------------------------
   Structure: the body is fractured, and the containment ring is visibly
   broken — a real gap you can see with the colour stripped out. Glyph is a
   cross. */
.orb-viz[data-status="failed"] {
  --_bezel-k: 0.85;
  --_state: var(--state-failed);
  --_bloom-k: 0.3;
  --_tint: 44%;
  --_body-op: 0.94;
  --_core-op: 0.3;
  --_core-size: 40%;
  --_halo-op: 0.3;
  --_bands-op: 0.2;
  --_fracture: 1;
  --_ring-w: 2.8;
  /* Three heavy segments with two wide gaps: unmistakably a broken shell. */
  --_arc-dash: 92;
  --_arc-gap: 21;
  --_glyph:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-width='3.2' stroke-linecap='round'%3E%3Cpath d='M6 6l12 12'/%3E%3Cpath d='M18 6 6 18'/%3E%3C/g%3E%3C/svg%3E");
}
.orb-viz[data-status="failed"] .orb-viz__ring-arc {
  stroke-dashoffset: 0;
}
.orb-viz[data-status="failed"] .orb-viz__ring-track {
  stroke-dasharray: 92 21;
  stroke-dashoffset: 0;
  transform: rotate(-90deg);
}
.orb-viz[data-status="failed"] .orb-viz__terminator::after {
  animation-name: var(--motion-off, orb-fracture);
  animation-duration: 620ms;
  animation-timing-function: var(--ease-snap);
  animation-iteration-count: 1;
}
.orb-viz[data-status="failed"] .orb-viz__specular { opacity: 0.55; }

/* ==========================================================================
   8. Size — presence, not diameter
   Bigger machines carry more internal structure, a heavier band, a deeper
   seat. Small ones are spare. The difference is legible across a mixed fleet
   without anyone measuring anything.
   ========================================================================== */

.orb-viz[data-size="a1.tiny"] {
  --_ticks: 18;
  --_bezel-op: 0.34;
  --_bands: 2;
  --_ring-k: 0.5;
  --_track-w: 0.7;
  --_core-k: 0.7;
  --_bands-k: 0.7;
}
.orb-viz[data-size="a1.small"] {
  --_ticks: 26;
  --_bezel-op: 0.42;
  --_bands: 4;
  --_ring-k: 0.75;
  --_track-w: 1;
  --_core-k: 0.86;
  --_bands-k: 0.85;
}
.orb-viz[data-size="a1.medium"] {
  --_ticks: 36;
  --_bezel-op: 0.5;
  --_bands: 6;
  --_ring-k: 1;
  --_track-w: 1.4;
  --_core-k: 1;
  --_bands-k: 1;
}
.orb-viz[data-size="a1.large"] {
  --_ticks: 52;
  --_bezel-op: 0.58;
  --_bands: 9;
  --_ring-k: 1.3;
  --_track-w: 1.9;
  --_core-k: 1.12;
  --_bands-k: 1.15;
}
.orb-viz[data-size="a1.xxlarge"] {
  --_ticks: 72;
  --_bezel-op: 0.66;
  --_bands: 13;
  --_ring-k: 1.62;
  --_track-w: 2.5;
  --_core-k: 1.24;
  --_bands-k: 1.3;
}

/* Apparent mass: the largest orbs sit deeper in their own shadow and hold a
   denser core; the smallest are light and thin-shelled. Applied on the body
   so it stacks with the state modelling rather than replacing it. */
.orb-viz[data-size="a1.tiny"] .orb-viz__body {
  box-shadow:
    inset 0 0 0 0.05em var(--_edge),
    inset -0.12em -0.16em 0.34em var(--_shade),
    0 0.14em 0.3em -0.12em var(--_shade);
}
.orb-viz[data-size="a1.large"] .orb-viz__body,
.orb-viz[data-size="a1.xxlarge"] .orb-viz__body {
  box-shadow:
    inset 0 0 0 0.09em var(--_edge),
    inset -0.26em -0.34em 0.72em var(--_shade),
    0 0.32em 0.7em -0.18em var(--_shade);
}
.orb-viz[data-size="a1.xxlarge"] .orb-viz__core {
  filter: saturate(1.15);
}

/* ==========================================================================
   9. Light theme
   On a near-white ground a luminous sphere cannot lean on bloom — it needs a
   drawn edge and real shadow-side modelling, and the halo has to nearly go
   away or it reads as a smudge.
   ========================================================================== */

:root[data-theme="light"] .orb-viz {
  --_shade: color-mix(in oklab, var(--fg) 26%, transparent);
  /* --_lit, --_hot and --_glow are NOT overridden here. They resolve towards
     --_incandescent, which is a warm white in both themes, so they are already
     correct on a pale page. (They used to resolve towards --fg, which inverts
     to near-black in light — the reason the core read as a smudge.) */
  --_mid:  color-mix(in oklab, var(--_state) 88%, var(--bg-raised));
  /* The shadow side resolves towards --fg-muted rather than --fg: on a pale
     page a near-black terminator makes every orb read as a hole punched in
     the sheet. This keeps the modelling and loses the weight. The ambient
     bias survives, so the limb still cools relative to the lit side. */
  --_dark: color-mix(in oklab, var(--_state) 76%,
             color-mix(in oklab, var(--_ambient) 10%, var(--fg-muted)));
  --_deep: color-mix(in oklab, var(--_state) 52%,
             color-mix(in oklab, var(--_ambient) 16%, var(--fg-muted)));
  --_edge: color-mix(in oklab, var(--_state) 70%, var(--fg));
  /* On a bright page the rim is mostly page-bounce, so it goes whiter and
     stronger. It is the only thing separating a dark orb from a dark
     terminator against a pale ground. */
  --_rim: color-mix(in oklab, var(--_incandescent) 62%, var(--_glow));
  /* Frost has to be a pale deposit on a dark-ish shell in dark theme and a
     cool grey deposit on a pale shell in light — --fg alone gets this exactly
     backwards on one of the two. */
  --_frost:     color-mix(in oklab, var(--_ambient) 46%, var(--fg-muted));
  /* And a crack is ink, not highlight, on a pale page. */
  --_cleft:     color-mix(in oklab, var(--fg) 66%, transparent);
  /* Restrained on purpose: against a pale, saturated shell a strong white lip
     out-reads the cleft itself and the break stops being a break and becomes
     a lightning bolt. The dark side has to dominate in both themes. */
  --_cleft-lip: color-mix(in oklab, var(--_incandescent) 40%, transparent);
}
:root[data-theme="light"] .orb-viz__halo {
  opacity: calc(var(--_halo-op) * 0.5);
}
/* Ready is the one state whose colour token is a neutral, so on a pale ground
   the generic light modelling would turn it into the heaviest, darkest orb on
   the page — the opposite of what it means. It becomes a pale silver pearl
   instead: unmistakably powered, unmistakably doing nothing. */
:root[data-theme="light"] .orb-viz[data-status="ready"] {
  --_lit:  color-mix(in oklab, var(--_state) 12%, var(--bg-raised));
  --_mid:  color-mix(in oklab, var(--_state) 32%, var(--bg-raised));
  --_dark: color-mix(in oklab, var(--_state) 60%, var(--bg-raised));
  --_deep: color-mix(in oklab, var(--_state) 88%, var(--bg-sunken));
  --_edge: color-mix(in oklab, var(--_state) 60%, transparent);
}
:root[data-theme="light"] .orb-viz[data-status="paused"] {
  --_lit:  color-mix(in oklab, var(--_state) 26%, var(--bg-raised));
  --_mid:  color-mix(in oklab, var(--_state) 48%, var(--bg-sunken));
  --_dark: color-mix(in oklab, var(--_state) 62%, var(--fg-faint));
  --_deep: color-mix(in oklab, var(--_state) 40%, var(--fg-muted));
}
:root[data-theme="light"] .orb-viz__glyph {
  /* Same construction as the dark plate — a milled part with a lit top edge —
     not a flat swatch. A bare background here would drop the gradient declared
     in section 6 entirely. */
  background:
    linear-gradient(196deg,
      color-mix(in oklab, var(--_glow) 12%, var(--bg-raised)) 0%,
      var(--bg-raised) 48%,
      color-mix(in oklab, var(--bg-sunken) 70%, var(--bg-raised)) 100%);
}

/* ==========================================================================
   10. Small-size guards
   Below ~56px the badge and the strata stop paying for themselves: the badge
   grows proportionally so its glyph never falls under ~7px, and the finest
   strata are dropped so they cannot alias into grey mush.
   ========================================================================== */

@container (max-width: 56px) {
  .orb-viz {
    /* A 40px orb has ~4px of mantle to glow through; push the bleed so the
       thing still reads as lit in a compact row. Separate scalar, never a
       self-reference: a custom property that reads itself is a cycle and
       resolves to the guaranteed-invalid value. */
    --_bloom-s: 1.3;
  }
  .orb-viz__glyph {
    /* Below ~56px the badge has to grow as a fraction — 2.9em of a 40px orb is
       11.6px, and the glyph inside it would fall to 7px. It also has to leave
       the ring and sit proud at the corner, because at this diameter a seated
       badge eats a quarter of the sphere. */
    --_badge: 4em;
    left: auto;
    top: auto;
    right: -0.4em;
    bottom: -0.4em;
    margin: 0;
  }
  .orb-viz__glyph::before { mask-size: 62% 62%; }
  .orb-viz__bands { opacity: calc(var(--_bands-op) * var(--_bands-k, 1) * 0.5); }
  .orb-viz__terminator::before { opacity: calc(var(--_crystal) * 0.75); }
}

/* --- The large renderings -------------------------------------------------
   The fleet card size and up. Two things change: the badge stops scaling with
   the orb (it converges on a fixed proportion of the RING, not of the sphere),
   and the internal strata pull back — at 220px+ a repeating horizontal rule
   set reads as scanlines painted on a disc, which is the exact opposite of the
   volume they exist to suggest. */

@container (min-width: 140px) {
  .orb-viz__glyph { --_badge: 2.35em; }
  .orb-viz__bands { opacity: calc(var(--_bands-op) * var(--_bands-k, 1) * 0.62); }
}

/* The hero and anatomy renderings. */
@container (min-width: 200px) {
  .orb-viz__glyph { --_badge: 2em; }
  /* The specular is NOT pulled back at the hero size any more. It used to be,
     because the old wide soft kick blew out; the tight one has to survive,
     because at 260px it is the single detail carrying "polished object". */
  .orb-viz__bands { opacity: calc(var(--_bands-op) * var(--_bands-k, 1) * 0.46); }
}
