@scope (spinner-ui) {
  /* ── Block 1 — TOKENS ─────────────────────────────────────────────
     Spinner inherits the icon ladder for sizing (matches icon-ui's
     `:scope[size="..."]` rems). The semantic colors come via `tone=…`
     re-assigning --spinner-color. Stroke is 2px (raw px allowed for
     borders 1-2px per component-token-contract §"Forbidden patterns"). */
  :where(:scope) {
    --spinner-size:           1rem;                       /* icon-ui md */
    --spinner-color:          currentColor;
    --spinner-stroke:         2px;
    /* Rotational cadence — interaction-duration tokens (`--a-duration-*`,
       120-300 ms) are tuned for hover/focus transitions; a full spinner
       revolution at 300 ms reads as a frantic blur. Canonical loader
       cadence is ~0.8 s per revolution (smooth, perceptible motion). */
    --spinner-duration:       0.8s;
    --spinner-track-opacity:  0.25;
    --spinner-dot-size:       calc(var(--spinner-size) / 4);
    --spinner-dot-gap:        calc(var(--spinner-size) / 8);
    /* Knight-rider variant — horizontal track with a sliding thumb.
       Track width is a separate scale from the round-spinner --spinner-size
       (Knight reads as a bar, not a glyph). Track height defaults to the
       spinner-stroke for visual continuity with arc/ring. Thumb width
       expressed as a 0-1 ratio of the track so the keyframe math is
       single-variable. */
    --spinner-bar-track-width:  4rem;
    --spinner-bar-track-height: var(--spinner-stroke);
    --spinner-bar-thumb-ratio:  0.3;
  }

  /* ── Block 2 — BASE ────────────────────────────────────────────── */
  :scope {
    box-sizing: border-box;
    display: inline-block;
    width:  var(--spinner-size);
    height: var(--spinner-size);
    color:  var(--spinner-color);
    vertical-align: middle;
    flex-shrink: 0;
  }

  /* ── Size ladder (mirrors icon-ui's local rem declarations) ─────── */
  :scope[size="sm"]  { --spinner-size: 0.875rem; }   /* 14px */
  :scope[size="md"]  { --spinner-size: 1rem; }       /* 16px */
  :scope[size="lg"]  { --spinner-size: 1.25rem; }    /* 20px */

  /* ── Tones (override --spinner-color token) ────────────────────── */
  :scope[tone="current"] { --spinner-color: currentColor; }
  :scope[tone="subtle"]  { --spinner-color: var(--a-fg-subtle); }
  :scope[tone="accent"]  { --spinner-color: var(--a-accent-strong); }
  :scope[tone="inverse"] { --spinner-color: var(--a-chrome-light); }

  /* ── Variant: ARC (default) — rotating quarter-circle border ───── */
  :scope[variant="arc"]::before,
  :scope:not([variant])::before {
    content: "";
    box-sizing: border-box;
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 50%;
    border: var(--spinner-stroke) solid;
    border-color: currentColor transparent transparent transparent;
    animation: spinner-ui-rotate var(--spinner-duration) linear infinite;
  }

  /* ── Variant: RING — full ring with one rotating colored segment ── */
  :scope[variant="ring"]::before {
    content: "";
    box-sizing: border-box;
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 50%;
    border: var(--spinner-stroke) solid;
    border-color: color-mix(in oklch, currentColor calc(var(--spinner-track-opacity) * 100%), transparent);
    border-top-color: currentColor;
    animation: spinner-ui-rotate var(--spinner-duration) linear infinite;
  }

  /* ── Variant: DOTS — three bouncing dots ─────────────────────────
     Layout: inline-flex with three real <span data-spinner-dot=N>
     children stamped by spinner.class.js #syncDots(). flex-gap distributes
     the row evenly; each child carries its own animation-delay so
     the bounce reads as a left-to-right wave.

     Why three real children (instead of ::before / ::after + a
     middle dot painted via box-shadow): box-shadow paints the
     middle dot but does NOT participate in flex-gap, so the box-
     shadow dot and the right pseudo had asymmetric spacing. The
     middle dot also couldn't carry its own @keyframes animation
     (box-shadow can't animate per-stop). Stamped children fix both. */
  :scope[variant="dots"] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: auto;             /* dots row is wider than tall */
    min-width: var(--spinner-size);
    gap: var(--spinner-dot-gap);
  }

  :scope[variant="dots"] > [data-spinner-dot] {
    width:  var(--spinner-dot-size);
    height: var(--spinner-dot-size);
    border-radius: 50%;
    background: currentColor;
    animation: spinner-ui-bounce var(--spinner-duration) ease-in-out infinite;
  }
  /* Stagger each dot so the row reads as a wave left → right.
     Three equal slices of one revolution (0, T/3, 2T/3) give the
     classic three-dot loading rhythm. */
  :scope[variant="dots"] > [data-spinner-dot="1"] { animation-delay: 0s; }
  :scope[variant="dots"] > [data-spinner-dot="2"] {
    animation-delay: calc(var(--spinner-duration) / 3);
  }
  :scope[variant="dots"] > [data-spinner-dot="3"] {
    animation-delay: calc(var(--spinner-duration) * 2 / 3);
  }

  /* ── Variant: KNIGHT — knight-rider sliding bar ──────────────────
     Horizontal track with a thumb sliding back-and-forth via
     `animation-direction: alternate`. Thumb width is a 0–1 ratio of
     the track so the keyframe math reduces to translateX(((1/ratio) - 1) * 100%)
     of the THUMB's own width — i.e. travel = (track - thumb) px,
     expressed in thumb-relative percentages so it scales with any
     track width without a calc that touches both axes. Default
     ratio 0.3 → translate end = 233.33% (of thumb width). */
  :scope[variant="knight"] {
    display: inline-block;
    width: var(--spinner-bar-track-width);
    height: var(--spinner-bar-track-height);
    background: color-mix(
      in oklch,
      currentColor calc(var(--spinner-track-opacity) * 100%),
      transparent
    );
    border-radius: 9999px;
    position: relative;
    overflow: hidden;
  }
  :scope[variant="knight"]::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;       /* top:0; right:auto; bottom:0; left:0 */
    width: calc(var(--spinner-bar-thumb-ratio) * 100%);
    background: currentColor;
    border-radius: inherit;
    animation: spinner-ui-knight
      var(--spinner-duration)
      ease-in-out infinite alternate;
  }

  /* ── Paused state — freeze in place ────────────────────────────── */
  :scope[paused]::before,
  :scope[paused]::after,
  :scope[paused] > [data-spinner-dot] {
    animation-play-state: paused;
  }

  /* ── Reduced-motion replacement: static "…" ellipsis ────────────
     WCAG 2.3.3 — animations of any duration are a vestibular risk.
     The static replacement still announces aria-busy="true" via the
     JS lifecycle. */
  @media (prefers-reduced-motion: reduce) {
    :scope::before,
    :scope::after,
    :scope > [data-spinner-dot] {
      animation: none !important;
    }
    :scope::before {
      content: "…";
      border: none;
      background: transparent;
      box-shadow: none;
      margin: 0;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      font: inherit;
      line-height: 1;
      text-align: center;
      color: currentColor;
    }
    /* Dots variant — hide all three stamped dots and let the host's
       ::before paint the ellipsis fallback. */
    :scope[variant="dots"] > [data-spinner-dot] { display: none; }
    :scope[variant="dots"] {
      width: var(--spinner-size);   /* shrink back to a square in reduced mode */
    }
    /* Knight-rider — hide the sliding thumb; the host's ::before
       above repaints as the ellipsis fallback. */
    :scope[variant="knight"] {
      width: var(--spinner-size);
      background: transparent;
    }
  }

  /* ── Keyframes ─────────────────────────────────────────────────── */
  @keyframes spinner-ui-rotate {
    to { transform: rotate(360deg); }
  }

  @keyframes spinner-ui-bounce {
    0%, 80%, 100% { transform: scale(0.6); opacity: 0.6; }
    40%           { transform: scale(1);   opacity: 1; }
  }

  /* Knight-rider — thumb slides from left edge to (track - thumb)
     and bounces back via `animation-direction: alternate`. The
     translate distance is expressed in thumb-relative percentages:
     translateX(((1 / ratio) - 1) * 100%) of the thumb's own width
     equals (track - thumb) px. At default ratio 0.3 this resolves
     to translateX(233.33%). */
  @keyframes spinner-ui-knight {
    from { transform: translateX(0); }
    to   { transform: translateX(
      calc((1 / var(--spinner-bar-thumb-ratio) - 1) * 100%)
    ); }
  }
}
