/* ── Per-project tokens (Demo brand). Two-tier: anchors → semantic. ────────
   Anchors are channel triples (`R G B`, not `rgb(R G B)`) so we can compose
   them with alpha (`rgb(var(--anchor) / 0.5)`) or opaque (`rgb(var(--anchor))`).
   Every downstream token is derived from an anchor, change the anchor, the
   whole family retints. */

@layer base, tokens, components;

/* ── Foundation. The baseline a project needs alongside the tokens: reset,
   color-scheme (dark mode), the focus ring, the reduced-motion guard, and the
   modal scroll-lock hook. Drop this file + a component's CSS into any project.
   (Also present in base.css, which the demo site loads for its own chrome.) */
@layer base {
  :root {
    color-scheme: light dark;

    /* Direction sign for transforms that can't use logical properties, so RTL
     mirrors them: multiply the offset by --flip. */
    --flip: 1;
  }

  :root[data-theme="dark"] {
    color-scheme: dark;
  }

  :root[data-theme="light"] {
    color-scheme: light;
  }

  [dir="rtl"] {
    --flip: -1;
  }

  * {
    box-sizing: border-box;
  }

  /* Body scroll-lock while a modal (Dialog/Sheet) is open. --scrollbar-comp
   (set in JS) reserves the removed scrollbar's width so the page never shifts. */
  html.is-scroll-locked {
    overflow: hidden;
    padding-inline-end: var(--scrollbar-comp, 0);
  }

  /* MD3-style focus ring: converges from a wider offset to its resting one. */
  @keyframes focus-ring-converge {
    from {
      outline-offset: 6px;
    }
  }

  :focus-visible {
    animation: focus-ring-converge var(--motion-default);
  }

  /* One global reduced-motion guard: near-instant (0.01ms so animationend still
   fires), single iteration, instant anchor scrolling. */
  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }

    /* Two exemptions: motion that conveys ongoing state must keep turning
     (slowed, where the component sets it) rather than freeze mid-cycle. The
     spinner is named because its markup needs no opt-in;
     [data-motion-essential] is the hook for anything else, including your own
     elements. Put it on the animating element itself, not an ancestor.
     Both have to live here, in the same rule that clamps everything else,
     because for !important declarations the cascade reverses layer order, so a
     later layer can never win. */
    *:not(.spinner, .spinner__arc, [data-motion-essential]),
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
    }
  }

  /* Shared keyboard focus ring at zero specificity; components override only
   what differs (offset, radius, or outline:none). */
  :where(a, button, summary, [tabindex], input, select, textarea, pre):focus-visible {
    outline: var(--focus-ring-width) solid var(--focus);
    outline-offset: var(--focus-ring-offset);
  }

  /* Visually hidden, still read by screen readers: supplies a spoken label
   when there's no visible one (e.g. a control's field name). */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
}

@layer tokens {
  :root {
    /* ── Anchors ─────────────────────────────────────────────────────────
     One channel triple per family. Change --anchor-neutral to retint the
     entire neutral system in one line, grays, ink, fills, overlay, and
     shadows all derive from it. Semantic anchors (brand + status) stay
     independent so retinting neutrals doesn't touch the brand palette. */
    --anchor-neutral: 40 40 40; /* drives grays, ink, fills, overlay, shadows */
    --anchor-primary: 108 43 245; /* brand, electric punch */

    /* Tints, tuned so text-on-own-soft-surface clears 4.5:1 without any family
     reading weaker than the others. Green and amber sit at rotated hues (144
     and 52) where sRGB offers more chroma than the "obvious" angles. Amber
     stays a warm rust rather than gold, 4.5:1 on white caps a text color at
     L≈0.55, and warm at that lightness is always brown. */
    --anchor-red: 191 0 0;
    --anchor-amber: 170 81 0;
    --anchor-green: 0 122 22;
    --anchor-blue: 25 80 200;
    --anchor-fuchsia: 152 39 160;

    /* Bases derived from the neutral anchor. Ink/fill flip to a light-tinted
     version in dark theme (see :root[data-theme="dark"] below) while these
     three all follow the anchor's hue. */
    --ink-base: rgb(var(--anchor-neutral));
    --fill-base: rgb(var(--anchor-neutral));
    --overlay-base: rgb(var(--anchor-neutral));

    /* ── Backgrounds ─────────────────────────────────────────────────────
     Opaque, but tinted by the anchor via color-mix. Primary = recessed
     page bg; secondary = elevated card/sheet surface. Retint the anchor
     → backgrounds shift subtly in the same direction. If a specific
     value doesn't look right after retinting, override the individual
     token directly (e.g. `--background-primary: rgb(244 242 241);`). */
    --background-primary: color-mix(in oklab, rgb(var(--anchor-neutral)) 4%, white);
    --background-primary-hover: color-mix(in oklab, rgb(var(--anchor-neutral)) 12%, white);
    --background-primary-active: color-mix(in oklab, rgb(var(--anchor-neutral)) 15%, white);
    --background-secondary: white;
    --background-secondary-hover: color-mix(in oklab, rgb(var(--anchor-neutral)) 8%, white);
    --background-secondary-active: color-mix(in oklab, rgb(var(--anchor-neutral)) 12%, white);

    /* ── Ink ─────────────────────────────────────────────────────────────
     Primary opaque. Secondary + tertiary are alpha of --ink-base so they
     blend into whatever surface sits underneath. Hover/active on primary
     fade toward the surface; on secondary/tertiary they nudge alpha up. */
    --ink-primary: var(--ink-base);
    --ink-primary-hover: color-mix(in oklab, var(--ink-primary) 88%, var(--background-secondary));
    --ink-primary-active: color-mix(in oklab, var(--ink-primary) 82%, var(--background-secondary));

    /* Opaque, not alpha: muted ink is mixed toward the base surface
     (--background-secondary, pure white in light / near-anchor in dark) instead
     of made translucent. Same percentages as the old alphas, so on the page it
     looks identical, but it no longer picks up whatever sits behind it, so a
     caption or a filled neutral surface reads the same gray on any surface.
     Matches how the big systems ship muted text (opaque steps, not alpha), and
     lets the neutral data-color use --ink-secondary as a real solid fill. */
    --ink-secondary: color-mix(in oklab, var(--ink-base) 70%, var(--background-secondary));
    --ink-secondary-hover: color-mix(in oklab, var(--ink-base) 81%, var(--background-secondary));
    --ink-secondary-active: color-mix(in oklab, var(--ink-base) 85%, var(--background-secondary));
    --ink-tertiary: color-mix(in oklab, var(--ink-base) 55%, var(--background-secondary));
    --ink-tertiary-hover: color-mix(in oklab, var(--ink-base) 66%, var(--background-secondary));
    --ink-tertiary-active: color-mix(in oklab, var(--ink-base) 70%, var(--background-secondary));

    /* ── Semantic ────────────────────────────────────────────────────────
     Each family opaque from its anchor; hover/active derived via color-mix
     toward white. Direction is consistent across themes. */
    --primary: rgb(var(--anchor-primary));
    --primary-hover: color-mix(in oklab, var(--primary) 88%, white);
    --primary-active: color-mix(in oklab, var(--primary) 82%, white);
    --red: rgb(var(--anchor-red));
    --red-hover: color-mix(in oklab, var(--red) 88%, white);
    --red-active: color-mix(in oklab, var(--red) 82%, white);

    /* Amber and green are nudged 6% darker than their anchors so the caption-size
     badge text (color on -soft) clears AA 4.5 in light; dark keeps the pure anchors
     (see the dark block) since darkening would cut its contrast on the dark stage. */
    --amber: color-mix(in oklab, rgb(var(--anchor-amber)) 94%, black);
    --amber-hover: color-mix(in oklab, var(--amber) 88%, white);
    --amber-active: color-mix(in oklab, var(--amber) 82%, white);
    --green: color-mix(in oklab, rgb(var(--anchor-green)) 94%, black);
    --green-hover: color-mix(in oklab, var(--green) 88%, white);
    --green-active: color-mix(in oklab, var(--green) 82%, white);
    --blue: rgb(var(--anchor-blue));
    --blue-hover: color-mix(in oklab, var(--blue) 88%, white);
    --blue-active: color-mix(in oklab, var(--blue) 82%, white);
    --fuchsia: rgb(var(--anchor-fuchsia));
    --fuchsia-hover: color-mix(in oklab, var(--fuchsia) 88%, white);
    --fuchsia-active: color-mix(in oklab, var(--fuchsia) 82%, white);

    /* Focus is a neutral, not the brand: near-black here, near-white in dark
     (see the dark block). A neutral ring stays high-contrast on any surface,
     including a [data-color] region, where a brand-tinted ring would clash or
     wash out. It rides the neutral anchor, so retinting --anchor-neutral tints
     the ring with everything else. */
    --focus: var(--neutral-black);
    --focus-hover: color-mix(in oklab, var(--focus) 88%, white);
    --focus-active: color-mix(in oklab, var(--focus) 82%, white);

    /* Focus-ring geometry, the ring's color is --focus; these are its shape.
     Every focus-visible outline in base.css/components.css reads these, so the
     ring's thickness and gap are tuned here in one place, not per component.
     Components with a bespoke offset (a tight inline control, a pill) keep their
     own outline-offset literal. */
    --focus-ring-width: 3px;
    --focus-ring-offset: 2px;

    /* Foreground: the color used for text/icons sitting on the -soft
     background. Only exists for primary (brand); tints use their
     solid color as text directly (only the brand accent has a
     foreground). In light theme foreground ≈ primary; in dark
     theme it's lightened for legibility on dark tinted surfaces. */
    --primary-foreground: var(--primary);
    --primary-foreground-hover: var(--primary-hover);
    --primary-foreground-active: var(--primary-active);

    /* Soft alpha variants, for chip/badge backgrounds where the tint sits
     behind foreground-color text. Hover/active ramp deeper.
       Each derives from a *lightened* base (`--<color>-tint`), not the raw
     accent: a dark, saturated accent at low alpha jumps hard as the alpha
     climbs, so the resting tint and its hover step feel heavy (this is why a
     dark, saturated purple felt off at low alpha). Lifting the base toward
     white first (one knob, `--soft-lift`, for every accent) keeps the tint
     gentle and the ramp even. Alpha ladder stays 0.12 / 0.23 / 0.27. */
    --soft-lift: 20%;
    --primary-tint: color-mix(in oklab, var(--primary), white var(--soft-lift));
    --red-tint: color-mix(in oklab, var(--red), white var(--soft-lift));
    --amber-tint: color-mix(in oklab, var(--amber), white var(--soft-lift));
    --green-tint: color-mix(in oklab, var(--green), white var(--soft-lift));
    --blue-tint: color-mix(in oklab, var(--blue), white var(--soft-lift));
    --fuchsia-tint: color-mix(in oklab, var(--fuchsia), white var(--soft-lift));

    --primary-soft: rgb(from var(--primary-tint) r g b / 0.12);
    --primary-soft-hover: rgb(from var(--primary-tint) r g b / 0.23);
    --primary-soft-active: rgb(from var(--primary-tint) r g b / 0.27);
    --red-soft: rgb(from var(--red-tint) r g b / 0.12);
    --red-soft-hover: rgb(from var(--red-tint) r g b / 0.23);
    --red-soft-active: rgb(from var(--red-tint) r g b / 0.27);
    --amber-soft: rgb(from var(--amber-tint) r g b / 0.12);
    --amber-soft-hover: rgb(from var(--amber-tint) r g b / 0.23);
    --amber-soft-active: rgb(from var(--amber-tint) r g b / 0.27);
    --green-soft: rgb(from var(--green-tint) r g b / 0.12);
    --green-soft-hover: rgb(from var(--green-tint) r g b / 0.23);
    --green-soft-active: rgb(from var(--green-tint) r g b / 0.27);
    --blue-soft: rgb(from var(--blue-tint) r g b / 0.12);
    --blue-soft-hover: rgb(from var(--blue-tint) r g b / 0.23);
    --blue-soft-active: rgb(from var(--blue-tint) r g b / 0.27);
    --fuchsia-soft: rgb(from var(--fuchsia-tint) r g b / 0.12);
    --fuchsia-soft-hover: rgb(from var(--fuchsia-tint) r g b / 0.23);
    --fuchsia-soft-active: rgb(from var(--fuchsia-tint) r g b / 0.27);

    /* ── Accent role ─────────────────────────────────────────────────────
     A contextual accent that defaults to the brand. Components read --accent*
     instead of a named color; wrapping any subtree in
     [data-color="primary|red|green|blue"] repoints --accent-anchor and the whole
     family re-derives from it. Only the anchor default lives here; the
     derivation is in the `:root, [data-color]` rule below, so it re-runs at the
     wrapper's scope (a custom property's var()s resolve on the element that
     declares it, so deriving only on :root would bake in the brand and ignore a
     descendant's anchor override). */
    --accent-anchor: var(--anchor-primary);

    /* Overlay, alpha of --overlay-base; ramps deeper for hover/active. */
    --overlay: rgb(from var(--overlay-base) r g b / 0.45);
    --overlay-hover: rgb(from var(--overlay-base) r g b / 0.56);
    --overlay-active: rgb(from var(--overlay-base) r g b / 0.6);

    /* ── Neutrals ────────────────────────────────────────────────────────
     Black is the neutral anchor; grays are anchor mixed with white at
     increasing ratios. Change --anchor-neutral to retint the whole ramp
     (stone, blue-gray, red-gray, etc.). White stays true white. */
    --neutral-black: rgb(var(--anchor-neutral));
    --neutral-black-hover: color-mix(in oklab, var(--neutral-black) 90%, white);
    --neutral-black-active: color-mix(in oklab, var(--neutral-black) 85%, white);
    --neutral-gray-1: color-mix(in oklab, var(--neutral-black) 22%, white);
    --neutral-gray-1-hover: color-mix(in oklab, var(--neutral-black) 30%, white);
    --neutral-gray-1-active: color-mix(in oklab, var(--neutral-black) 34%, white);
    --neutral-gray-2: color-mix(in oklab, var(--neutral-black) 15%, white);
    --neutral-gray-2-hover: color-mix(in oklab, var(--neutral-black) 22%, white);
    --neutral-gray-2-active: color-mix(in oklab, var(--neutral-black) 26%, white);
    --neutral-gray-3: color-mix(in oklab, var(--neutral-black) 9%, white);
    --neutral-gray-3-hover: color-mix(in oklab, var(--neutral-black) 16%, white);
    --neutral-gray-3-active: color-mix(in oklab, var(--neutral-black) 19%, white);
    --neutral-gray-4: color-mix(in oklab, var(--neutral-black) 4%, white);
    --neutral-gray-4-hover: color-mix(in oklab, var(--neutral-black) 11%, white);
    --neutral-gray-4-active: color-mix(in oklab, var(--neutral-black) 14%, white);
    --neutral-white: white;
    --neutral-white-hover: color-mix(in oklab, white 90%, var(--neutral-black));
    --neutral-white-active: color-mix(in oklab, white 87%, var(--neutral-black));

    /* ── Fills ───────────────────────────────────────────────────────────
     Alpha tints of --fill-base, sit over any surface (hover overlays,
     selected rows). Each tier has its own hover/active alphas so
     fill-backed controls move through interactive states without
     swapping tokens. */
    --fill-primary: rgb(from var(--fill-base) r g b / 0.26);
    --fill-primary-hover: rgb(from var(--fill-base) r g b / 0.37);
    --fill-primary-active: rgb(from var(--fill-base) r g b / 0.41);
    --fill-secondary: rgb(from var(--fill-base) r g b / 0.2);
    --fill-secondary-hover: rgb(from var(--fill-base) r g b / 0.31);
    --fill-secondary-active: rgb(from var(--fill-base) r g b / 0.35);
    --fill-tertiary: rgb(from var(--fill-base) r g b / 0.12);
    --fill-tertiary-hover: rgb(from var(--fill-base) r g b / 0.23);
    --fill-tertiary-active: rgb(from var(--fill-base) r g b / 0.27);
    --fill-quaternary: rgb(from var(--fill-base) r g b / 0.08);
    --fill-quaternary-hover: rgb(from var(--fill-base) r g b / 0.17);
    --fill-quaternary-active: rgb(from var(--fill-base) r g b / 0.21);

    /* Type, roles in three tiers: Headline / Body / Caption. Line-heights are
     unitless multipliers so they scale with the font-size. Fixed at every
     breakpoint, sizes don't step up on desktop. */
    --font-weight-regular: 400;
    --font-weight-medium: 500;
    --font-weight-semibold: 600;

    --font-headline-large-size: 3.75rem; /* 60px */
    --font-headline-large-lh: 1.2;
    --font-headline-large-weight: 500;
    --font-headline-medium-size: 3rem; /* 48px */
    --font-headline-medium-lh: 1.2;
    --font-headline-medium-weight: 500;
    --font-headline-small-size: 2.375rem; /* 38px */
    --font-headline-small-lh: 1.2;
    --font-headline-small-weight: 500;

    --font-title-large-size: 2rem; /* 32px */
    --font-title-large-lh: 1.3;
    --font-title-large-weight: 500;
    --font-title-medium-size: 1.75rem; /* 28px */
    --font-title-medium-lh: 1.3;
    --font-title-medium-weight: 500;
    --font-title-small-size: 1.5rem; /* 24px */
    --font-title-small-lh: 1.3;
    --font-title-small-weight: 500;

    --font-body-large-size: 1.25rem; /* 20px */
    --font-body-large-lh: 1.5;
    --font-body-large-weight: 400;
    --font-body-large-emphasized-size: 1.25rem;
    --font-body-large-emphasized-lh: 1.5;
    --font-body-large-emphasized-weight: 500;
    --font-body-medium-size: 1.125rem; /* 18px */
    --font-body-medium-lh: 1.5;
    --font-body-medium-weight: 400;
    --font-body-medium-emphasized-size: 1.125rem;
    --font-body-medium-emphasized-lh: 1.5;
    --font-body-medium-emphasized-weight: 500;
    --font-body-small-size: 1rem; /* 16px */
    --font-body-small-lh: 1.5;
    --font-body-small-weight: 400;
    --font-body-small-emphasized-size: 1rem;
    --font-body-small-emphasized-lh: 1.5;
    --font-body-small-emphasized-weight: 500;

    --font-caption-medium-size: 0.875rem; /* 14px */
    --font-caption-medium-lh: 1.5;
    --font-caption-medium-weight: 400;
    --font-caption-medium-emphasized-size: 0.875rem;
    --font-caption-medium-emphasized-lh: 1.5;
    --font-caption-medium-emphasized-weight: 500;
    --font-caption-small-size: 0.75rem; /* 12px */
    --font-caption-small-lh: 1.5;
    --font-caption-small-weight: 400;
    --font-caption-small-emphasized-size: 0.75rem;
    --font-caption-small-emphasized-lh: 1.5;
    --font-caption-small-emphasized-weight: 500;

    /* Breakpoints: standard scale (matches Tailwind). Reach for the name; the
     px is the min-width the layout switches at. */
    --breakpoint-sm: 640px;
    --breakpoint-md: 768px;
    --breakpoint-lg: 1024px;
    --breakpoint-xl: 1280px;
    --breakpoint-2xl: 1536px;

    /* Grid (mobile-first), content max 1280 (aligns with the xl breakpoint) */
    --container-max: 1280px;
    --grid-columns: 4;
    --grid-gutter: 16px;
    --grid-margin: 16px;

    /* Spacing scale, named by px value (4dp base); rem so it scales with root font */
    --space-0: 0;
    --space-2: 0.125rem;
    --space-4: 0.25rem;
    --space-8: 0.5rem;
    --space-12: 0.75rem;
    --space-16: 1rem;
    --space-20: 1.25rem;
    --space-24: 1.5rem;
    --space-32: 2rem;
    --space-48: 3rem;
    --space-64: 4rem;
    --space-80: 5rem;
    --space-96: 6rem;
    --space-112: 7rem;
    --space-128: 8rem;

    /* Border width, the hairline every outlined surface (inputs, cards,
     dividers) shares. One knob so borders can thicken or retint with the
     system instead of being frozen at 1px in dozens of rules. */
    --border-width: 1px;

    /* Radii, on the spacing rhythm (8/16/24) */
    --radius-sm: 8px;
    --radius-md: 16px;
    --radius-lg: 24px;
    --radius-xl: 32px;
    --radius-full: 999px;

    /* Icon masks reused across components. Place with
     `mask: var(--icon-x) center / contain no-repeat` and tint via `background`.
     Defined once so a shape is swapped in one place, not re-inlined per rule. */
    --icon-chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6l6 -6'/%3E%3C/svg%3E");
    --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l5 5l10 -10'/%3E%3C/svg%3E");

    /* Density, [data-size="xs|sm|md|lg"] on any ancestor nudges the label size
     of density-aware components (see .chip) via this addition. Component pixel
     sizes are set explicitly per size in the component itself (see .button's
     --button-size ladder). Default matches `data-size="md"`; the xs/sm/lg
     overrides live at the end of the file. */
    --ds-font-size-addition: 0rem;

    /* Shadows (Tailwind scale, layered for realistic depth). Color is
     mixed from --neutral-black so shadows follow any future tint of the
     neutrals (and stay dark in both themes, --ink flips, --neutral-black
     doesn't unless --anchor-neutral itself is changed). Each ends with a soft,
     zero-offset ambient layer so the glow wraps all edges (incl. the top),
     keeping an elevated surface visible even when what's behind it matches. */
    --shadow-sm:
      0 1px 3px 0 color-mix(in oklab, var(--neutral-black) 10%, transparent),
      0 1px 2px -1px color-mix(in oklab, var(--neutral-black) 10%, transparent),
      0 0 6px 0 color-mix(in oklab, var(--neutral-black) 5%, transparent);
    --shadow-md:
      0 4px 6px -1px color-mix(in oklab, var(--neutral-black) 10%, transparent),
      0 2px 4px -2px color-mix(in oklab, var(--neutral-black) 10%, transparent),
      0 0 12px 0 color-mix(in oklab, var(--neutral-black) 7%, transparent);
    --shadow-lg:
      0 10px 15px -3px color-mix(in oklab, var(--neutral-black) 10%, transparent),
      0 4px 6px -4px color-mix(in oklab, var(--neutral-black) 10%, transparent),
      0 0 16px 0 color-mix(in oklab, var(--neutral-black) 8%, transparent);

    /* Top-edge rim for elevated surfaces. A shadow defines the edge in light, so
     this is a no-op here; dark overrides it with a faint light highlight (the
     one cue that reads on a dark surface). Append after the shadow token. */
    --surface-rim: inset 0 0 0 transparent;

    /* Motion, one composed default for hovers/state changes; raw easings for
     entrances (ease-out) and exits (ease-in) that need a different curve. */
    --motion-default: 150ms cubic-bezier(0.4, 0, 0.2, 1);
    --motion-ease-in: cubic-bezier(0.4, 0, 1, 1);
    --motion-ease-out: cubic-bezier(0, 0, 0.2, 1);

    /* Z-index, stacking ladder. Reach for the semantic name, not the number;
     gaps of 10 leave room to slot custom values between. */
    --z-base: 0;
    --z-sticky: 10;
    --z-dropdown: 20;
    --z-overlay: 30;
    --z-modal: 40;
    --z-toast: 50;
  }

  /* ── Contextual accent (data-color) ──────────────────────────────────────
   The accent family is derived here, on both :root and every [data-color], so
   the solid/hover/soft/foreground tokens re-compute at whichever scope sets the
   anchor. (Declaring them only on :root would resolve --accent-anchor once, at
   :root, and inherit the baked brand result down past any wrapper override.)
   Same machinery as the named accents; --soft-lift and --neutral-white are
   inherited in, so they resolve at each scope too. */
  :root,
  [data-color] {
    --accent: rgb(var(--accent-anchor));

    /* Light lifts a bit harder (86/80, not 88/82): a colored fill on a bright
     page shifts less perceptibly on hover than a light fill on the dark stage,
     so light needs a stronger nudge to match dark's felt change. Dark holds at
     88/82, reset in the dark block below. Step stays 6 between hover and active.
     Neutral runs on --ink-primary-hover/active (already the strongest lift), so
     it's left as is. */
    --accent-hover: color-mix(in oklab, var(--accent) 86%, white);
    --accent-active: color-mix(in oklab, var(--accent) 80%, white);

    /* Text/icon on the solid accent fill, black or white by the accent's own
     lightness so it stays legible on any tint. round(1.15 - l) resolves to 1
     (white) for darker accents like the brand violet or a light-theme red, and
     0 (black) for light accents like the dark-theme tints. Chroma 0 keeps it a
     neutral, hue is irrelevant. */
    --accent-on: oklch(from var(--accent) clamp(0, round(1.15 - l), 1) 0 h);
    --accent-foreground: var(--accent); /* text on the -soft surface (dark lightens) */
    --accent-foreground-hover: var(--accent-hover);
    --accent-foreground-active: var(--accent-active);
    --accent-tint: color-mix(in oklab, var(--accent), white var(--soft-lift));
    --accent-soft: rgb(from var(--accent-tint) r g b / 0.12);
    --accent-soft-hover: rgb(from var(--accent-tint) r g b / 0.23);
    --accent-soft-active: rgb(from var(--accent-tint) r g b / 0.27);

    /* Border for outline controls in a region: the region's own color, so an
     outlined button/input inside [data-color] reads in that color and flips
     per theme with --accent. Neutral repoints it to the standard control border
     (--ink-tertiary) below. Not consumed by a component here yet, it's the role
     primitive for outline treatments (kept per the full-API principle). */
    --accent-border: var(--accent);
  }

  /* Repoint the accent per subtree. Region contexts: brand (primary), muted
   (neutral), destructive (red), plus green/blue. All but neutral just swap the
   anchor and let the family re-derive (anchors flip per theme, so dark is free);
   neutral is special (see below). Names stay primitive (green/blue, not
   success/info); other color tokens (--amber, …) still exist for direct use
   without being region contexts. "primary" is the default, listed so an inner
   wrapper can reset a nested region back. */
  [data-color="primary"] {
    --accent-anchor: var(--anchor-primary);
  }

  /* Neutral can't use --accent-anchor: --anchor-neutral is a fixed dark value
   (the ink/shadow source) that doesn't flip per theme, so it'd be a dark button
   on a dark page. Instead point the accent family straight at the ink/fill
   tokens, which already flip light↔dark, so a neutral region reads in both.
   --accent-on stays adaptive, so filled-neutral text auto-contrasts. */
  [data-color="neutral"] {
    --accent: var(--ink-primary);
    --accent-hover: var(--ink-primary-hover);
    --accent-active: var(--ink-primary-active);

    /* Filled fill stays full ink in both themes: our dark colored fills are
     bright pastels, so a bright (near-white) neutral sits with them, where a
     mid-gray would read dull and off-tone. Soft-surface text is full ink too,
     so it matches the fill (as the coloured regions do, where -foreground =
     -accent) and its own hover/active, which already resolve to full ink. */
    --accent-foreground: var(--ink-primary);
    --accent-soft: var(--fill-tertiary);
    --accent-soft-hover: var(--fill-tertiary-hover);
    --accent-soft-active: var(--fill-tertiary-active);

    /* Neutral outline uses the standard control border, not the near-black
     fill, so a neutral outlined control matches every input/button. */
    --accent-border: var(--ink-tertiary);
  }

  [data-color="red"] {
    --accent-anchor: var(--anchor-red);
  }

  [data-color="green"] {
    --accent-anchor: var(--anchor-green);
  }

  [data-color="blue"] {
    --accent-anchor: var(--anchor-blue);
  }

  /* Tablet ≥768 (md), grid jumps to 12 columns (px literals: media can't read vars) */
  @media (min-width: 768px) {
    :root {
      --grid-columns: 12;
      --grid-gutter: 20px;
      --grid-margin: 24px;
    }
  }

  /* Desktop ≥1024, wider grid margins (type stays fixed across breakpoints). */
  @media (min-width: 1024px) {
    :root {
      --grid-margin: 32px;
    }
  }

  /* ── Dark theme ────────────────────────────────────────────────────────────
   Ink + fill anchors flip to light-on-dark. Semantic anchors soften. Gray
   ratios re-tuned so grays read against dark surfaces. Backgrounds swap to
   dark. Everything else derives automatically. */

  :root[data-theme="dark"] {
    /* Ink/fill flip to a light-tinted version of the neutral anchor,
     if you retint the anchor to blue, dark-theme text becomes hint-of-blue
     near-white, not pure white. */
    --ink-base: color-mix(in oklab, rgb(var(--anchor-neutral)) 6%, white);
    --fill-base: color-mix(in oklab, rgb(var(--anchor-neutral)) 8%, white);

    /* Scrim goes pure black: the neutral anchor is a mid-gray, which barely
     darkens an already-dark page, so the overlay could never dim on dark.
     Black (like the shadows) actually recedes the content behind a modal. */
    --overlay-base: #000;

    --anchor-primary: 118 55 255;

    /* Focus flips to near-white on the dark stage (light theme uses near-black).
     --neutral-white is pure white; hold it a hair off so the ring reads as a
     ring, not a glare, while still clearing contrast on any dark surface. */
    --focus: color-mix(in oklab, var(--neutral-white) 92%, var(--neutral-black));

    /* Tints, matched perceptual weight (C≈0.11-0.16) so no family pops harder. */
    --anchor-red: 255 150 150;
    --anchor-amber: 234 170 80;
    --anchor-green: 84 203 153;
    --anchor-blue: 140 190 255;
    --anchor-fuchsia: 235 157 239;

    /* Use the anchors directly here; the light theme darkens amber/green for AA on
     -soft badge text, but on the dark stage that would reduce contrast, not help. */
    --amber: rgb(var(--anchor-amber));
    --green: rgb(var(--anchor-green));

    /* Foreground: only primary has one. Lightened for dark-theme
     legibility on soft bg. Mixed 60% because a violet primary sits dark,
     so it needs more white to read as bright text. Then re-saturated:
     oklch holds L (so contrast is unchanged) and hue, but pushes chroma
     1.5× so the light text reads violet, not washed-out lilac. */
    --primary-foreground: oklch(
      from color-mix(in oklab, var(--primary), white 60%) l calc(c * 1.5) h
    );

    /* Hover/active DARKEN here rather than lighten like the light theme, because
     the dark-theme foreground is already a bright lilac, so pushing it lighter
     would wash out. A pure oklch lightness drop (hue + chroma held) keeps the
     lilac saturated instead of graying it. Direction flips per theme; the light
     block above lightens via --primary-hover/-active. Used by the link hover and
     the button--foreground fill. */
    --primary-foreground-hover: oklch(from var(--primary-foreground) calc(l - 0.06) c h);
    --primary-foreground-active: oklch(from var(--primary-foreground) calc(l - 0.1) c h);

    /* Soft variants, higher alpha for visibility against dark surfaces.
     No color-mix lightening needed because the dark tint solids are
     already lifted (red/green/blue chosen to work as text on soft bg,
     so alpha of them reads fine as bg too).

     Primary runs 5 alpha points above the rest of the families: it's the
     only deep solid left in dark (a violet, not a light tint), so the same
     alpha lifts far less luminance off the surface and the resting
     secondary button reads too flat. */
    --primary-soft: rgb(from color-mix(in oklab, var(--primary), white 30%) r g b / 0.28);
    --primary-soft-hover: rgb(from color-mix(in oklab, var(--primary), white 30%) r g b / 0.39);
    --primary-soft-active: rgb(from color-mix(in oklab, var(--primary), white 30%) r g b / 0.43);

    /* Red drops 2 alpha points, it's the lightest solid, so its surface has
     to run paler to hold 4.5:1. */
    --red-soft: rgb(from var(--red) r g b / 0.18);
    --red-soft-hover: rgb(from var(--red) r g b / 0.29);
    --red-soft-active: rgb(from var(--red) r g b / 0.33);
    --amber-soft: rgb(from var(--amber) r g b / 0.2);
    --amber-soft-hover: rgb(from var(--amber) r g b / 0.31);
    --amber-soft-active: rgb(from var(--amber) r g b / 0.35);
    --green-soft: rgb(from var(--green) r g b / 0.2);
    --green-soft-hover: rgb(from var(--green) r g b / 0.31);
    --green-soft-active: rgb(from var(--green) r g b / 0.35);
    --blue-soft: rgb(from var(--blue) r g b / 0.2);
    --blue-soft-hover: rgb(from var(--blue) r g b / 0.31);
    --blue-soft-active: rgb(from var(--blue) r g b / 0.35);
    --fuchsia-soft: rgb(from var(--fuchsia) r g b / 0.2);
    --fuchsia-soft-hover: rgb(from var(--fuchsia) r g b / 0.31);
    --fuchsia-soft-active: rgb(from var(--fuchsia) r g b / 0.35);

    /* Dark backgrounds derive from the same anchor. Primary mixes anchor
     with black; secondary/hover/active add hints of white to lift. */
    --background-primary: color-mix(in oklab, rgb(var(--anchor-neutral)) 90%, black);
    --background-primary-hover: color-mix(in oklab, rgb(var(--anchor-neutral)) 93%, white);
    --background-primary-active: color-mix(in oklab, rgb(var(--anchor-neutral)) 89%, white);
    --background-secondary: color-mix(in oklab, rgb(var(--anchor-neutral)) 97%, white);
    --background-secondary-hover: color-mix(in oklab, rgb(var(--anchor-neutral)) 86%, white);
    --background-secondary-active: color-mix(in oklab, rgb(var(--anchor-neutral)) 83%, white);

    /* Overlay ramps deeper for dark-theme scrim */
    --overlay: rgb(from var(--overlay-base) r g b / 0.64);
    --overlay-hover: rgb(from var(--overlay-base) r g b / 0.75);
    --overlay-active: rgb(from var(--overlay-base) r g b / 0.79);

    /* Grays lifted higher against a dark surface, anchor mixed further toward
     white than in light theme. --neutral-black stays anchor-defined (dark)
     so shadows keep their weight. */
    --neutral-gray-1: color-mix(in oklab, var(--neutral-black) 62%, white);
    --neutral-gray-1-hover: color-mix(in oklab, var(--neutral-black) 55%, white);
    --neutral-gray-1-active: color-mix(in oklab, var(--neutral-black) 52%, white);
    --neutral-gray-2: color-mix(in oklab, var(--neutral-black) 70%, white);
    --neutral-gray-2-hover: color-mix(in oklab, var(--neutral-black) 62%, white);
    --neutral-gray-2-active: color-mix(in oklab, var(--neutral-black) 58%, white);
    --neutral-gray-3: color-mix(in oklab, var(--neutral-black) 76%, white);
    --neutral-gray-3-hover: color-mix(in oklab, var(--neutral-black) 68%, white);
    --neutral-gray-3-active: color-mix(in oklab, var(--neutral-black) 64%, white);
    --neutral-gray-4: color-mix(in oklab, var(--neutral-black) 82%, white);
    --neutral-gray-4-hover: color-mix(in oklab, var(--neutral-black) 74%, white);
    --neutral-gray-4-active: color-mix(in oklab, var(--neutral-black) 70%, white);

    /* Same shape as light, offsets, alphas, and the ambient edge layer all
     match. The only difference is color: pure black instead of the neutral
     tint, so the shadow still reads on a dark surface (a one-off: dark shadows
     don't follow the neutral tint). */
    --shadow-sm:
      0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1), 0 0 6px 0 rgb(0 0 0 / 0.05);
    --shadow-md:
      0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 12px 0 rgb(0 0 0 / 0.07);
    --shadow-lg:
      0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1),
      0 0 16px 0 rgb(0 0 0 / 0.08);

    /* A dark shadow can't define a top edge on a dark page, so catch the top rim
     with a faint light highlight instead, the dark-theme counterpart to the
     drop shadow (see --surface-rim in :root). */
    --surface-rim:
      inset 0 1px 0 rgb(255 255 255 / 0.04), inset 1px 0 0 rgb(255 255 255 / 0.02),
      inset -1px 0 0 rgb(255 255 255 / 0.02);
  }

  /* Dark-theme accent overrides, applied at both the root and any [data-color]
   scope so a wrapped region re-derives them from its own anchor (the light
   family in `:root, [data-color]` above already re-derives; these override the
   foreground + soft for dark). Mirrors primary's dark treatment so the default
   brand accent is identical to --primary*: lift toward white then re-saturate
   for foreground text, and derive the soft surface from a white-lifted accent
   so a deep violet or a light tint both read against the dark stage. Neutral is
   excluded: it already points at flip-per-theme ink/fill tokens, so this
   chromatic treatment would wrongly recolour it. */
  :root[data-theme="dark"],
  :root[data-theme="dark"] [data-color]:not([data-color="neutral"]) {
    /* Hold hover/active at the gentler 88/82: on the dark stage a light fill
     already reads a clear hover, so it doesn't need the light-theme 84/78 lift. */
    --accent-hover: color-mix(in oklab, var(--accent) 88%, white);
    --accent-active: color-mix(in oklab, var(--accent) 82%, white);
    --accent-foreground: oklch(
      from color-mix(in oklab, var(--accent), white 60%) l calc(c * 1.5) h
    );

    /* Hover/active DARKEN (oklch L drop, hue+chroma held), mirroring
     --primary-foreground-* in dark: the foreground is a light tint here, so
     lightening would wash out. Keeps accent-foreground* === primary-foreground*
     at root. */
    --accent-foreground-hover: oklch(from var(--accent-foreground) calc(l - 0.06) c h);
    --accent-foreground-active: oklch(from var(--accent-foreground) calc(l - 0.1) c h);
    --accent-soft: rgb(from color-mix(in oklab, var(--accent), white 30%) r g b / 0.28);
    --accent-soft-hover: rgb(from color-mix(in oklab, var(--accent), white 30%) r g b / 0.39);
    --accent-soft-active: rgb(from color-mix(in oklab, var(--accent), white 30%) r g b / 0.43);
  }

  /* ── Density knob ──────────────────────────────────────────────────────────
   Set `data-size="xs|sm|md|lg"` on any ancestor to shift the label size of
   density-aware components below that node (component pixel sizes are set
   explicitly per size in the component). Root default matches md. Each step is
   2px, so the label lands on a real type token at every tier (12/14/16/18). */

  [data-size="xs"] {
    --ds-font-size-addition: -0.25rem;
  }

  [data-size="sm"] {
    --ds-font-size-addition: -0.125rem;
  }

  [data-size="md"] {
    --ds-font-size-addition: 0rem;
  }

  [data-size="lg"] {
    --ds-font-size-addition: 0.125rem;
  }

  /* ── Container width tier ───────────────────────────────────────────────────
   The default content cap is xl (--container-max, 1280). A page opts into the
   wider 2xl tier with `data-width="2xl"` on <html> (like data-theme). Anything
   reading --container-max (page content, the card grid, the footer) follows,
   so their edges stay aligned. Only the cap changes; columns/gutters/margins,
   radii, and type are identical across tiers. */
  [data-width="2xl"] {
    --container-max: var(--breakpoint-2xl);
  }
}
