@scope (loading-overlay-ui) {
  /* ── Block 1 — TOKENS ─────────────────────────────────────────────
     Loading-overlay covers a positioned parent: backdrop tint, corner
     radius, stacking order, and motion. Variants below re-point these
     component-named tokens (per component-token-contract.md). */
  :where(:scope) {
    --loading-overlay-bg:        var(--a-scrim-default);
    --loading-overlay-radius:    var(--a-radius-md);
    --loading-overlay-z:         50;
    --loading-overlay-duration:  var(--a-duration);
    --loading-overlay-easing:    var(--a-easing-out);
    --loading-overlay-gap:       var(--a-space-3);
  }

  /* ── Block 2 — BASE
     Idle state: hidden but mounted. The element stays in the DOM so
     state transitions on `[active]` animate; `display: none` would
     skip the transition entirely. We pair `opacity: 0` with
     `pointer-events: none` so the dim overlay doesn't intercept
     clicks on the underlying content when idle. */
  :scope {
    box-sizing: border-box;
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    gap: var(--loading-overlay-gap);
    background: var(--loading-overlay-bg);
    border-radius: var(--loading-overlay-radius);
    z-index: var(--loading-overlay-z);
    opacity: 0;
    pointer-events: none;
    transition:
      opacity var(--loading-overlay-duration)
              var(--loading-overlay-easing);
  }

  /* ── Active state — fade in + capture pointer events ─────────────
     Active state is gated by the delay timer in JS (see loading-overlay.class.js); by
     the time `[active]` is set, the grace window has elapsed and the
     overlay should paint immediately on next frame. */
  :scope[active] {
    opacity: 1;
    pointer-events: auto;
  }

  /* ── Variants ─────────────────────────────────────────────────────
     transparent: no backdrop fill — useful when the indicator IS the
     overlay (e.g. a full-area skeleton stack). The parent is still
     marked aria-busy and pointer events still blocked.
     blur: light scrim plus backdrop-filter blur on the underlying
     content. Use sparingly — Safari < 18 paint cost. */
  :scope[variant="transparent"] {
    --loading-overlay-bg: transparent;
  }

  :scope[variant="blur"] {
    --loading-overlay-bg: var(--a-scrim-weak);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
  }

  /* ── Auto-stamped spinner sizing ──────────────────────────────────
     When the default slot is empty, the JS layer auto-stamps a
     `<spinner-ui size="lg">` child. Style it as the default centered
     indicator so consumer slots and the auto-stamp render identically. */
  :scope > spinner-ui {
    --spinner-color: var(--a-fg-muted);
  }

  /* ── Reduced motion ───────────────────────────────────────────────
     WCAG 2.3.3 — suppress the fade. The overlay still paints / hides
     instantly (no transition), and the auto-stamped spinner falls
     back to its own reduced-motion ellipsis via spinner.css. */
  @media (prefers-reduced-motion: reduce) {
    :scope {
      transition: none;
    }
  }
}
