/* Welkin — canonical cascade-layer order (docs/04-css-architecture.md).
   This declaration is public API: reordering is a breaking change.
   Every source file repeats this exact line first, so any subset of files
   concatenates correctly in any order (docs/decisions/ADR-0003). */
@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin reset (ADR-0004): own minimal reset, zero specificity via :where(),
   lowest layer — nothing a user writes can lose to it. Scope is fixed by the
   ADR; opinions beyond normalisation belong in `base`. Documented line by line
   in docs/04-css-architecture.md. */
@layer reset {
  /* Predictable sizing everywhere, including generated content. */
  :where(*, *::before, *::after) {
    box-sizing: border-box;
  }

  /* Margin trimming: default margins fight the .stack primitive's owned
     rhythm (docs/06); spacing is a layout decision, not an element default. */
  :where(*) {
    margin: 0;
  }

  /* Top-layer elements are centred by the UA via margin: auto against
     inset: 0 — that is layout, not flow rhythm, so the trim above must not
     strip it (a zero-margin dialog/popover pins to the top-left corner). */
  :where(dialog, [popover]) {
    margin: auto;
  }

  /* Root defaults. Unitless line-height inherits proportionally.
     -webkit-text-size-adjust: iOS Safari inflates text in landscape without
     it (vendor/physical form is the interoperable one — justified exception
     per ADR-0009). scroll-padding keeps in-page navigation targets clear of
     sticky chrome (WCAG 2.4.11 Focus Not Obscured); the navbar's block-size
     token feeds it, with a small fallback gap for pages without a bar. */
  :where(html) {
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    scroll-padding-block-start: var(--wel-navbar-block-size, 1rem);
  }

  /* Smooth in-page scrolling as motion: opt-in under the user's preference
     only — the additive rule from docs/03. The --wel-motion multiplier
     cannot zero a discrete keyword, so the preference gates it directly. */
  @media (prefers-reduced-motion: no-preference) {
    :where(html) {
      scroll-behavior: smooth;
    }
  }

  /* Media defaults: block display kills phantom baseline gaps; max-inline-size
     keeps replaced elements inside their container; block-size auto preserves
     aspect ratio when a width attribute is present. */
  :where(img, picture, video, canvas, svg) {
    display: block;
    max-inline-size: 100%;
    block-size: auto;
  }

  /* Form controls inherit the surrounding voice instead of UA chrome fonts. */
  :where(input, button, textarea, select) {
    font: inherit;
  }

  /* Headings bind as visual units; paragraphs avoid orphans. Core-tier candy
     (docs/03) — engines without these text-wrap values keep normal wrapping. */
  :where(h1, h2, h3, h4, h5, h6) {
    text-wrap: balance;
  }

  :where(p) {
    text-wrap: pretty;
  }

  /* Long words/URLs must never force container overflow. */
  :where(p, h1, h2, h3, h4, h5, h6) {
    overflow-wrap: break-word;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin colour tokens (docs/05). Three tiers:
   primitives (oklch ramps) -> semantic roles (light-dark(), typed) -> component
   tokens (defined in each component's file). Components never read primitives.
   Token names are semver-governed API. */

/* ---- @property registrations (semantic colour tokens are typed: docs/05).
   initial-value is the concrete light-scheme value — registrations cannot
   carry var()/light-dark(), so these back-stop the :root declarations. */
@layer tokens {
  /* ---- Scheme wiring (ADR-0007, amended 1.0.1). [data-theme] on any subtree
     pins the scheme — page-level toggle and scoped mixed-mode regions with one
     mechanism. A JS toggle only ever sets data-theme.
     Because the semantic tokens are typed (@property "<color>"), light-dark()
     and the accent derivations resolve at computed-value time ON THE ELEMENT
     THAT DECLARES THEM — descendants inherit the already-resolved colour. A
     pin that only set color-scheme was therefore inert for tokens (T-84): the
     token block below re-declares on every [data-theme] subtree root, so the
     whole palette re-resolves against the pinned scheme and the locally
     cascaded accent. Consequence for themes: declare on `:root, [data-theme]`
     (doc 10) or a :root-only theme stops at the first data-theme boundary.
     The pin rules live AFTER the token block: equal specificity, so source
     order is what lets them beat its `color-scheme: light dark`.
     Fallback stance: engines without light-dark() resolve each typed token
     to its @property initial-value — the static light palette (ADR-0007). */
  @property --wel-color-surface        { syntax: "<color>"; inherits: true; initial-value: oklch(97% 0.002 250); }
  @property --wel-color-surface-raised { syntax: "<color>"; inherits: true; initial-value: oklch(99.5% 0.001 250); }
  @property --wel-color-surface-sunken { syntax: "<color>"; inherits: true; initial-value: oklch(94% 0.003 250); }
  @property --wel-color-ink            { syntax: "<color>"; inherits: true; initial-value: oklch(23% 0.004 250); }
  @property --wel-color-ink-muted      { syntax: "<color>"; inherits: true; initial-value: oklch(46% 0.009 250); }
  @property --wel-color-ink-faint      { syntax: "<color>"; inherits: true; initial-value: oklch(62% 0.01 250); }
  @property --wel-color-accent          { syntax: "<color>"; inherits: true; initial-value: oklch(54% 0.16 255); }
  @property --wel-color-accent-hover    { syntax: "<color>"; inherits: true; initial-value: oklch(48% 0.16 255); }
  @property --wel-color-accent-active   { syntax: "<color>"; inherits: true; initial-value: oklch(44% 0.16 255); }
  @property --wel-color-accent-tint     { syntax: "<color>"; inherits: true; initial-value: oklch(92% 0.021 255); }
  @property --wel-color-accent-contrast { syntax: "<color>"; inherits: true; initial-value: oklch(100% 0 0); }
  @property --wel-color-border        { syntax: "<color>"; inherits: true; initial-value: oklch(88% 0.005 250); }
  @property --wel-color-border-strong { syntax: "<color>"; inherits: true; initial-value: oklch(62% 0.01 250); }
  @property --wel-color-info    { syntax: "<color>"; inherits: true; initial-value: oklch(46% 0.144 255); }
  @property --wel-color-success { syntax: "<color>"; inherits: true; initial-value: oklch(46% 0.135 150); }
  @property --wel-color-warning { syntax: "<color>"; inherits: true; initial-value: oklch(46% 0.117 80); }
  @property --wel-color-danger  { syntax: "<color>"; inherits: true; initial-value: oklch(46% 0.153 25); }
  @property --wel-color-info-tint    { syntax: "<color>"; inherits: true; initial-value: oklch(91% 0.019 255); }
  @property --wel-color-success-tint { syntax: "<color>"; inherits: true; initial-value: oklch(91% 0.018 150); }
  @property --wel-color-warning-tint { syntax: "<color>"; inherits: true; initial-value: oklch(91% 0.016 80); }
  @property --wel-color-danger-tint  { syntax: "<color>"; inherits: true; initial-value: oklch(91% 0.02 25); }

  /* ---- Primitives: oklch ramps, written out in full (ADR-0002 — no
     generation at build time; a docs-phase script may rewrite this block).
     Fixed lightness targets per step, per-hue chroma curve peaking mid-ramp,
     so every ramp reads evenly spaced. Never consumed by components. */
  :root, [data-theme] {
    color-scheme: light dark;

    /* grey (hue 250, near-neutral) */
    --wel-grey-50:  oklch(97% 0.002 250);
    --wel-grey-100: oklch(94% 0.003 250);
    --wel-grey-200: oklch(88% 0.005 250);
    --wel-grey-300: oklch(80% 0.007 250);
    --wel-grey-400: oklch(71% 0.009 250);
    --wel-grey-500: oklch(62% 0.01 250);
    --wel-grey-600: oklch(54% 0.01 250);
    --wel-grey-700: oklch(46% 0.009 250);
    --wel-grey-800: oklch(38% 0.008 250);
    --wel-grey-900: oklch(30% 0.006 250);
    --wel-grey-950: oklch(23% 0.004 250);
    --wel-grey-975: oklch(17% 0.003 250);

    /* blue (hue 255) — default accent + info */
    --wel-blue-50:  oklch(97% 0.024 255);
    --wel-blue-100: oklch(94% 0.04 255);
    --wel-blue-200: oklch(88% 0.072 255);
    --wel-blue-300: oklch(80% 0.104 255);
    --wel-blue-400: oklch(71% 0.136 255);
    --wel-blue-500: oklch(62% 0.16 255);
    --wel-blue-600: oklch(54% 0.16 255);
    --wel-blue-700: oklch(46% 0.144 255);
    --wel-blue-800: oklch(38% 0.12 255);
    --wel-blue-900: oklch(30% 0.088 255);
    --wel-blue-950: oklch(23% 0.064 255);
    --wel-blue-975: oklch(17% 0.048 255);

    /* green (hue 150) — success */
    --wel-green-50:  oklch(97% 0.023 150);
    --wel-green-100: oklch(94% 0.038 150);
    --wel-green-200: oklch(88% 0.068 150);
    --wel-green-300: oklch(80% 0.098 150);
    --wel-green-400: oklch(71% 0.128 150);
    --wel-green-500: oklch(62% 0.15 150);
    --wel-green-600: oklch(54% 0.15 150);
    --wel-green-700: oklch(46% 0.135 150);
    --wel-green-800: oklch(38% 0.113 150);
    --wel-green-900: oklch(30% 0.083 150);
    --wel-green-950: oklch(23% 0.06 150);
    --wel-green-975: oklch(17% 0.045 150);

    /* amber (hue 80) — warning */
    --wel-amber-50:  oklch(97% 0.02 80);
    --wel-amber-100: oklch(94% 0.033 80);
    --wel-amber-200: oklch(88% 0.059 80);
    --wel-amber-300: oklch(80% 0.085 80);
    --wel-amber-400: oklch(71% 0.111 80);
    --wel-amber-500: oklch(62% 0.13 80);
    --wel-amber-600: oklch(54% 0.13 80);
    --wel-amber-700: oklch(46% 0.117 80);
    --wel-amber-800: oklch(38% 0.098 80);
    --wel-amber-900: oklch(30% 0.072 80);
    --wel-amber-950: oklch(23% 0.052 80);
    --wel-amber-975: oklch(17% 0.039 80);

    /* red (hue 25) — danger */
    --wel-red-50:  oklch(97% 0.026 25);
    --wel-red-100: oklch(94% 0.043 25);
    --wel-red-200: oklch(88% 0.077 25);
    --wel-red-300: oklch(80% 0.111 25);
    --wel-red-400: oklch(71% 0.145 25);
    --wel-red-500: oklch(62% 0.17 25);
    --wel-red-600: oklch(54% 0.17 25);
    --wel-red-700: oklch(46% 0.153 25);
    --wel-red-800: oklch(38% 0.128 25);
    --wel-red-900: oklch(30% 0.094 25);
    --wel-red-950: oklch(23% 0.068 25);
    --wel-red-975: oklch(17% 0.051 25);

    /* ---- Semantic roles. Each defined ONCE with light-dark() (ADR-0007);
       [data-theme] pinning + the graduation-audit fallback are T-17's concern.
       Interaction shades are computed, not enumerated (docs/05 derivation):
       one --wel-color-accent override retints hover/active/tint/contrast. */
    --wel-color-surface:        light-dark(var(--wel-grey-50), var(--wel-grey-975));
    --wel-color-surface-raised: light-dark(oklch(99.5% 0.001 250), var(--wel-grey-950));
    --wel-color-surface-sunken: light-dark(var(--wel-grey-100), oklch(13% 0.003 250));

    --wel-color-ink:       light-dark(var(--wel-grey-950), var(--wel-grey-100));
    --wel-color-ink-muted: light-dark(var(--wel-grey-700), var(--wel-grey-400));
    --wel-color-ink-faint: light-dark(var(--wel-grey-500), var(--wel-grey-600));

    --wel-color-border:        light-dark(var(--wel-grey-200), var(--wel-grey-800));
    /* border-strong must hold 3:1 vs surface in both schemes (WCAG 1.4.11) */
    --wel-color-border-strong: light-dark(var(--wel-grey-500), var(--wel-grey-500));

    --wel-color-accent:          light-dark(var(--wel-blue-600), var(--wel-blue-400));
    /* Interaction shades: fixed lightness steps. Below l 0.12 the step
       flips to LIGHTEN, so a near-black accent still shows hover/active
       feedback — clamp(0, (l - 0.12) * infinity, 1) is 1 above the cut,
       0 below (the same flip idiom as -contrast under it). */
    --wel-color-accent-hover:    oklch(from var(--wel-color-accent) calc(l + 0.06 - 0.12 * clamp(0, (l - 0.12) * infinity, 1)) c h);
    --wel-color-accent-active:   oklch(from var(--wel-color-accent) calc(l + 0.10 - 0.20 * clamp(0, (l - 0.12) * infinity, 1)) c h);
    --wel-color-accent-tint:     color-mix(in oklch, var(--wel-color-accent) 12%, var(--wel-color-surface));
    /* Text-on-accent, DERIVED (T-130, docs/05): white when the accent's
       linear luminance y < 0.179, black above. That cut is the only band
       where both flip sides clear 4.5:1 (white text needs y <= 0.183,
       black needs y >= 0.175), so ANY in-gamut accent keeps the
       primary-button pairing — including pure black and pure white.
       xyz-d65 y is the specified colour's luminance (pre-gamut-mapping);
       0.9505/1.089 are the D65 white point, so the flip lands on exact
       white or black. check-contrast.mjs sweeps this guarantee in CI.
       Engines without relative colour drop the declaration and fall back
       to the @property initial-value (white), matching the static light
       palette stance (ADR-0007). */
    --wel-color-accent-contrast: color(from var(--wel-color-accent) xyz-d65
                                   calc(clamp(0, (0.179 - y) * infinity, 1) * 0.9505)
                                   clamp(0, (0.179 - y) * infinity, 1)
                                   calc(clamp(0, (0.179 - y) * infinity, 1) * 1.089));

    /* Tones (data-tone axes): text-safe base in both schemes; tints follow
       the surface automatically via color-mix, like accent-tint. */
    --wel-color-info:    light-dark(var(--wel-blue-700), var(--wel-blue-300));
    --wel-color-success: light-dark(var(--wel-green-700), var(--wel-green-300));
    --wel-color-warning: light-dark(var(--wel-amber-700), var(--wel-amber-300));
    --wel-color-danger:  light-dark(var(--wel-red-700), var(--wel-red-300));

    --wel-color-info-tint:    color-mix(in oklch, var(--wel-color-info) 12%, var(--wel-color-surface));
    --wel-color-success-tint: color-mix(in oklch, var(--wel-color-success) 12%, var(--wel-color-surface));
    --wel-color-warning-tint: color-mix(in oklch, var(--wel-color-warning) 12%, var(--wel-color-surface));
    --wel-color-danger-tint:  color-mix(in oklch, var(--wel-color-danger) 12%, var(--wel-color-surface));
  }

  /* ---- Image dimming (T-102): photos glare on dark surfaces. Filter
     values cannot ride light-dark(), so the resolved scheme is
     reconstructed the pin-compatible way: OS preference here, then the
     pin rules below re-declare per [data-theme] subtree root exactly like
     the palette (T-84) — they sit after this block so source order lets a
     pin beat the OS preference on the same element. The light value is
     the IDENTITY filter, never `none`: `none` inside a longer filter list
     invalidates the whole declaration, and consumers compose this token
     (.dim, .duotone combo — utilities.css). Nothing applies the token by
     itself; the .dim utility (or the user's own img rule) opts media in. */
  :root {
    /* the dark-scheme amount, themable in one place */
    --wel-img-dim-amount: brightness(0.85) contrast(1.05);
    --wel-img-dim: brightness(1);
  }

  @media (prefers-color-scheme: dark) {
    :root {
      --wel-img-dim: var(--wel-img-dim-amount);
    }
  }

  /* Scheme pins — after the token block on purpose (see the wiring note):
     same specificity as its [data-theme] arm, so source order is what makes
     the pin win over `color-scheme: light dark` (and the dim token's OS
     arm above). */
  [data-theme="light"] {
    color-scheme: light;
    --wel-img-dim: brightness(1);
  }

  [data-theme="dark"] {
    color-scheme: dark;
    --wel-img-dim: var(--wel-img-dim-amount);
  }

  /* Forced colors: the user demanded the system palette — a scoped scheme
     pin is meaningless there, and Chromium resolves forced CanvasText per
     the pinned scheme while forcing backgrounds to the page Canvas, which
     turns a dark pin into white-on-white. Un-pin so text and background
     force consistently. (After the pins: same specificity, source order.) */
  @media (forced-colors: active) {
    [data-theme] {
      color-scheme: light dark;
    }

    /* Dimming off too — never alter the user's demanded rendering. */
    :root,
    [data-theme] {
      --wel-img-dim: brightness(1);
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin focus-ring tokens (docs/05, docs/09). Consumed by the single
   global :focus-visible rule in `base` and nothing else. Widths/offsets are
   px hairline-class values (ADR-0008): the ring must stay crisp, and its
   visibility must not depend on user font size. */
@layer tokens {
  :root {
    --wel-focus-ring-color: var(--wel-color-accent);
    --wel-focus-ring-width: 2px;
    --wel-focus-ring-offset: 2px;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin motion tokens (docs/05). The multiplier rule: every duration in
   the codebase is written calc(duration-token * var(--wel-motion)) — no raw
   durations anywhere (lint-checked). Zero-duration transitions still fire
   transitionend, so JS never needs a reduced-motion branch. */
@layer tokens {
  @property --wel-motion { syntax: "<number>"; inherits: true; initial-value: 1; }

  :root {
    --wel-motion: 1;

    --wel-motion-duration-1: 100ms;
    --wel-motion-duration-2: 200ms;
    --wel-motion-duration-3: 300ms;
    --wel-motion-duration-4: 500ms;

    --wel-motion-ease: cubic-bezier(0.2, 0, 0.2, 1);
    --wel-motion-ease-in: cubic-bezier(0.4, 0, 1, 1);
    --wel-motion-ease-out: cubic-bezier(0, 0, 0.2, 1);
    --wel-motion-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  @media (prefers-reduced-motion: reduce) {
    :root {
      --wel-motion: 0;
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin radius, border, and shadow tokens (docs/05). Radii are typed
   lengths; borders are px hairlines by decision (ADR-0008 — physical
   crispness, deliberately non-scaling); shadows are composite values built
   from one light-dark() shadow colour so dark mode deepens every elevation
   step through a single token (ADR-0007). */
@layer tokens {
  @property --wel-radius-sm   { syntax: "<length>"; inherits: true; initial-value: 0.25rem; }
  @property --wel-radius-md   { syntax: "<length>"; inherits: true; initial-value: 0.5rem; }
  @property --wel-radius-lg   { syntax: "<length>"; inherits: true; initial-value: 1rem; }
  @property --wel-radius-full { syntax: "<length>"; inherits: true; initial-value: 999rem; }
  @property --wel-radius-control { syntax: "<length>"; inherits: true; initial-value: 0.375rem; }
  @property --wel-radius-surface { syntax: "<length>"; inherits: true; initial-value: 0.75rem; }
  @property --wel-shadow-color { syntax: "<color>"; inherits: true; initial-value: oklch(25% 0.01 250 / 0.15); }

  /* `:root, [data-theme]` — --wel-shadow-color is typed, so its light-dark()
     resolves where declared; re-declaring on pinned subtrees keeps dark-mode
     elevation working inside them (T-84, same mechanism as tokens/color.css). */
  :root, [data-theme] {
    --wel-radius-sm: 0.25rem;
    --wel-radius-md: 0.5rem;
    --wel-radius-lg: 1rem;
    --wel-radius-full: 999rem;
    /* Semantic radii — themes change product personality here. */
    --wel-radius-control: 0.375rem;
    --wel-radius-surface: 0.75rem;

    --wel-border-width: 1px;
    --wel-border-width-strong: 2px;

    /* Elevation. Shadows barely read on dark surfaces, so the dark value is
       deeper and more opaque — one token, both schemes. */
    --wel-shadow-color: light-dark(oklch(25% 0.01 250 / 0.15), oklch(5% 0.01 250 / 0.55));
    --wel-shadow-1: 0 1px 2px var(--wel-shadow-color);
    --wel-shadow-2: 0 2px 6px var(--wel-shadow-color);
    --wel-shadow-3: 0 6px 16px var(--wel-shadow-color);
    --wel-shadow-4: 0 12px 32px var(--wel-shadow-color);
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin space tokens (docs/05, ADR-0008). Same fluid mechanism as type:
   step n = anchor x multiplier (1, 2, 3, 4, 6, 8, 12, 16, 24), each a
   clamp() between compact (anchor-min 0.25rem) and generous
   (anchor-max 0.3125rem) across a 20rem -> 80rem inline viewport, so
   layouts breathe proportionally at every size. Endpoints derive from the
   typed anchors — re-declaring the two anchors re-densifies the system. */
@layer tokens {
  @property --wel-space-anchor-min { syntax: "<length>"; inherits: true; initial-value: 0.25rem; }
  @property --wel-space-anchor-max { syntax: "<length>"; inherits: true; initial-value: 0.3125rem; }

  :root {
    --wel-space-anchor-min: 0.25rem;
    --wel-space-anchor-max: 0.3125rem;

    /* x1 */
    --wel-space-1: clamp(var(--wel-space-anchor-min), 0.2292rem + 0.1042vi, var(--wel-space-anchor-max));
    /* x2 */
    --wel-space-2: clamp(calc(var(--wel-space-anchor-min) * 2), 0.4583rem + 0.2083vi, calc(var(--wel-space-anchor-max) * 2));
    /* x3 */
    --wel-space-3: clamp(calc(var(--wel-space-anchor-min) * 3), 0.6875rem + 0.3125vi, calc(var(--wel-space-anchor-max) * 3));
    /* x4 */
    --wel-space-4: clamp(calc(var(--wel-space-anchor-min) * 4), 0.9167rem + 0.4167vi, calc(var(--wel-space-anchor-max) * 4));
    /* x6 */
    --wel-space-5: clamp(calc(var(--wel-space-anchor-min) * 6), 1.375rem + 0.625vi, calc(var(--wel-space-anchor-max) * 6));
    /* x8 */
    --wel-space-6: clamp(calc(var(--wel-space-anchor-min) * 8), 1.8333rem + 0.8333vi, calc(var(--wel-space-anchor-max) * 8));
    /* x12 */
    --wel-space-7: clamp(calc(var(--wel-space-anchor-min) * 12), 2.75rem + 1.25vi, calc(var(--wel-space-anchor-max) * 12));
    /* x16 */
    --wel-space-8: clamp(calc(var(--wel-space-anchor-min) * 16), 3.6667rem + 1.6667vi, calc(var(--wel-space-anchor-max) * 16));
    /* x24 */
    --wel-space-9: clamp(calc(var(--wel-space-anchor-min) * 24), 5.5rem + 2.5vi, calc(var(--wel-space-anchor-max) * 24));

    /* Semantic alias: default container padding. */
    --wel-space-gutter: var(--wel-space-5);
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin typography tokens (docs/05, ADR-0008). Fluid modular scale:
   each step is clamp(min, preferred, max) interpolating between a
   small-context size (anchor-min x ratio-min^step, ratio 1.2 minor third)
   and a large-context size (anchor-max x ratio-max^step, ratio 1.25 major
   third) across a 20rem -> 80rem inline viewport. rem + vi inside clamp()
   keeps zoom working (WCAG 1.4.4).

   clamp endpoints are calc()-derived from the anchors, so re-declaring the
   two anchor tokens re-densifies the whole scale (ADR-0008); the preferred
   term's slope is written out numerically for the default anchors — a
   docs-phase generator may rewrite this file (ADR-0002), never the build. */
@layer tokens {
  /* Anchors are typed lengths; ratio tokens document the scale and feed the
     regeneration script. */
  @property --wel-text-anchor-min { syntax: "<length>"; inherits: true; initial-value: 1rem; }
  @property --wel-text-anchor-max { syntax: "<length>"; inherits: true; initial-value: 1.125rem; }

  :root {
    --wel-text-anchor-min: 1rem;
    --wel-text-anchor-max: 1.125rem;
    --wel-text-ratio-min: 1.2;
    --wel-text-ratio-max: 1.25;

    /* step -1: min = anchor-min / 1.2, max = anchor-max / 1.25 */
    --wel-text-size--1: clamp(calc(var(--wel-text-anchor-min) * 0.8333), 0.8111rem + 0.1111vi, calc(var(--wel-text-anchor-max) * 0.8));
    /* step 0: the anchors themselves (body size) */
    --wel-text-size-0: clamp(var(--wel-text-anchor-min), 0.9583rem + 0.2083vi, var(--wel-text-anchor-max));
    /* step 1: ratio^1 -> 1.2 / 1.25 */
    --wel-text-size-1: clamp(calc(var(--wel-text-anchor-min) * 1.2), 1.1313rem + 0.3438vi, calc(var(--wel-text-anchor-max) * 1.25));
    /* step 2: ratio^2 -> 1.44 / 1.5625 */
    --wel-text-size-2: clamp(calc(var(--wel-text-anchor-min) * 1.44), 1.3341rem + 0.5297vi, calc(var(--wel-text-anchor-max) * 1.5625));
    /* step 3: ratio^3 -> 1.728 / 1.9531 */
    --wel-text-size-3: clamp(calc(var(--wel-text-anchor-min) * 1.728), 1.5716rem + 0.7822vi, calc(var(--wel-text-anchor-max) * 1.9531));
    /* step 4: ratio^4 -> 2.0736 / 2.4414 */
    --wel-text-size-4: clamp(calc(var(--wel-text-anchor-min) * 2.0736), 1.8493rem + 1.1217vi, calc(var(--wel-text-anchor-max) * 2.4414));
    /* step 5: ratio^5 -> 2.4883 / 3.0518 */
    --wel-text-size-5: clamp(calc(var(--wel-text-anchor-min) * 2.4883), 2.1733rem + 1.5748vi, calc(var(--wel-text-anchor-max) * 3.0518));
    /* step 6: ratio^6 -> 2.986 / 3.8147 */
    --wel-text-size-6: clamp(calc(var(--wel-text-anchor-min) * 2.986), 2.5508rem + 2.1758vi, calc(var(--wel-text-anchor-max) * 3.8147));

    /* Font stacks — system stacks, no bundled webfonts (docs/05). */
    --wel-text-font-body: system-ui, sans-serif;
    --wel-text-font-display: var(--wel-text-font-body);
    --wel-text-font-mono: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;

    /* Leading and tracking companions. */
    --wel-text-leading-tight: 1.25;
    --wel-text-leading-normal: 1.5;
    --wel-text-leading-loose: 1.75;
    --wel-text-tracking-tight: -0.01em;
    --wel-text-tracking-normal: 0;
    --wel-text-tracking-wide: 0.04em;

    /* Max line length; consumed by .prose and .center (they agree by
       construction — docs/components/prose.md). */
    --wel-text-measure: 65ch;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin base form defaults (docs/04): light element normalisation only.
   The full field/control treatment (borders, sizes, validation states) is
   the form-controls component (docs/components/form-controls.md); base makes
   bare controls presentable and scheme-correct with zero classes. */
@layer base {
  /* Retint native widgets (checks, radios, progress, range) from the accent
     token — the docs/components quick path; dark mode and forced colors stay
     native via color-scheme. */
  input, textarea, select, progress, meter {
    accent-color: var(--wel-color-accent);
  }

  ::placeholder {
    color: var(--wel-color-ink-faint);
    opacity: 1; /* Firefox default dims via opacity; the token carries the value */
  }

  fieldset {
    border: var(--wel-border-width) solid var(--wel-color-border);
    border-radius: var(--wel-radius-md);
    padding-block: var(--wel-space-3);
    padding-inline: var(--wel-space-4);
  }

  legend {
    padding-inline: var(--wel-space-1);
  }

  button:not(:disabled),
  select:not(:disabled),
  input:where([type="checkbox"], [type="radio"], [type="range"]):not(:disabled) {
    cursor: pointer;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin base media defaults (docs/04). Sizing/display normalisation lives
   in the reset; base adds the light presentational voice. */
@layer base {
  figcaption {
    color: var(--wel-color-ink-muted);
    font-size: var(--wel-text-size--1);
  }

  iframe {
    border: none;
    max-inline-size: 100%;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin base table defaults (docs/04): a bare <table> reads as a table —
   hairline row rules, start-aligned headers. The full component (scroller,
   variants, sticky, sorting) is docs/components/table.md. */
@layer base {
  table {
    border-collapse: collapse;
  }

  caption {
    color: var(--wel-color-ink-muted);
    font-size: var(--wel-text-size--1);
    text-align: start;
  }

  th {
    text-align: start;
    font-weight: 600;
  }

  th, td {
    border-block-end: var(--wel-border-width) solid var(--wel-color-border);
    padding-block: var(--wel-space-1);
    padding-inline: var(--wel-space-2);
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* Welkin base typography (docs/04): opinionated element defaults — the
   "classless-ish" experience. Elements are made presentable everywhere;
   editorial rhythm (inter-block spacing, measure) is .prose's job, not
   base's. No class selectors in this layer. */
@layer base {
  body {
    background: var(--wel-color-surface);
    color: var(--wel-color-ink);
    font-family: var(--wel-text-font-body);
    font-size: var(--wel-text-size-0);
    line-height: var(--wel-text-leading-normal);
  }

  /* A data-theme scope pins its colour scheme (ADR-0007, tokens/color.css);
     painting surface + ink here is what makes the pinned region visibly its
     own scheme rather than only retinting native widgets. */
  [data-theme] {
    background: var(--wel-color-surface);
    color: var(--wel-color-ink);
  }

  /* The single global focus ring (docs/09): token-driven, zero-specificity,
     :focus-visible only — never rings on mouse click, never removed.
     Components restyle it via the three tokens, nothing else. */
  :where(:focus-visible) {
    outline: var(--wel-focus-ring-width) solid var(--wel-focus-ring-color);
    outline-offset: var(--wel-focus-ring-offset);
  }

  /* Heading scale: h1..h6 -> size-5..size-0 on the fluid scale (docs/05);
     .prose adopts the same mapping so documents agree with app chrome. */
  h1, h2, h3, h4, h5, h6 {
    font-family: var(--wel-text-font-display);
    line-height: var(--wel-text-leading-tight);
  }

  h1 { font-size: var(--wel-text-size-5); letter-spacing: var(--wel-text-tracking-tight); }
  h2 { font-size: var(--wel-text-size-4); letter-spacing: var(--wel-text-tracking-tight); }
  h3 { font-size: var(--wel-text-size-3); }
  h4 { font-size: var(--wel-text-size-2); }
  h5 { font-size: var(--wel-text-size-1); }
  h6 { font-size: var(--wel-text-size-0); }

  small { font-size: var(--wel-text-size--1); }

  a {
    color: var(--wel-color-accent);
    text-decoration-thickness: 0.08em;
    text-underline-offset: 0.15em;
  }

  a:hover {
    color: var(--wel-color-accent-hover);
    text-decoration-thickness: 0.14em;
  }

  code, kbd, samp, pre {
    font-family: var(--wel-text-font-mono);
    font-size: 0.925em;
  }

  code, kbd {
    background: var(--wel-color-surface-sunken);
    border-radius: var(--wel-radius-sm);
    padding-block: 0.1em;
    padding-inline: 0.3em;
  }

  pre {
    background: var(--wel-color-surface-sunken);
    border-radius: var(--wel-radius-md);
    padding: var(--wel-space-3);
    overflow-x: auto; /* physical form permitted until overflow-inline graduates (ADR-0009) */
  }

  /* Inline code inside a pre block must not double up the treatment. */
  pre code {
    background: none;
    border-radius: 0;
    padding: 0;
    font-size: 1em;
  }

  hr {
    border: none;
    border-block-start: var(--wel-border-width) solid var(--wel-color-border);
  }

  mark {
    background: var(--wel-color-warning-tint);
    color: var(--wel-color-ink);
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .center — horizontally centred, measure-capped content column (docs/06,
   T-26). A three-track grid with named lines rather than auto margins, so
   a data-breakout child (full-bleed figure, edge-to-edge section) can
   escape the measure to the full container width while normal flow stays
   capped. Default cap agrees with .prose by construction: both read
   --wel-text-measure. Knobs: --wel-center-max, --wel-center-gutter. */
@layer layout {
  .center {
    display: grid;
    grid-template-columns:
      [full-start] minmax(var(--wel-center-gutter, var(--wel-space-gutter)), 1fr)
      [content-start] min(var(--wel-center-max, var(--wel-text-measure)), 100% - 2 * var(--wel-center-gutter, var(--wel-space-gutter)))
      [content-end] minmax(var(--wel-center-gutter, var(--wel-space-gutter)), 1fr)
      [full-end];
    container-type: inline-size;
    container-name: layout;
  }

  .center > :where(*) {
    grid-column: content;
  }

  .center > :where([data-breakout]) {
    grid-column: full;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .cluster — horizontal grouping that wraps (docs/06, T-22): tag lists,
   button rows, nav items. Knobs: --wel-cluster-gap, --wel-cluster-align,
   --wel-cluster-justify. Establishes a queryable container per the doctrine
   (docs/06 §doctrine #3/#4). */
@layer layout {
  .cluster {
    display: flex;
    flex-wrap: wrap;
    gap: var(--wel-cluster-gap, var(--wel-space-3));
    align-items: var(--wel-cluster-align, center);
    justify-content: var(--wel-cluster-justify, flex-start);
    container-type: inline-size;
    container-name: layout;
  }

  /* Guardrail (ADR-0013): a nested primitive's inline-size containment
     makes its content contribute nothing to flex-basis: auto, so as a
     cluster item it would collapse to zero width. Growing is the only
     content-independent size a wrapping row can give it; zero width is
     never intent, so this only ever replaces a broken render. flex-basis
     stays auto: an explicit inline-size on the item is still honoured as
     the basis, and :where() keeps any author rule winning. */
  .cluster > :where(.stack, .cluster, .grid, .switcher, .sidebar-layout, .center, .cover, .frame) {
    flex-grow: 1;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .cover — full-height cover with a vertically centred principal element
   (docs/06, T-27): heroes, splash sections. Mark the principal child with
   data-principal; its auto block margins centre it while header/footer
   children keep to the edges. Knobs: --wel-cover-min-height,
   --wel-cover-padding. */
@layer layout {
  .cover {
    display: flex;
    flex-direction: column;
    gap: var(--wel-space-4);
    min-block-size: var(--wel-cover-min-height, 100dvb);
    padding: var(--wel-cover-padding, var(--wel-space-5));
    container-type: inline-size;
    container-name: layout;
  }

  .cover > :where([data-principal]) {
    margin-block: auto;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .frame — aspect-ratio media frame with object-fit cropping (docs/06,
   T-28): thumbnails, video embeds, art-directed images that must hold a
   shape regardless of intrinsic media size. Knob: --wel-frame-ratio. */
@layer layout {
  .frame {
    aspect-ratio: var(--wel-frame-ratio, 16 / 9);
    overflow: hidden;
    container-type: inline-size;
    container-name: layout;
  }

  .frame > :where(img, video, picture) {
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .grid — auto-fit card grid: as many min-wide columns as fit (docs/06,
   T-25). The min(100%, …) guard keeps a track from forcing overflow in
   containers narrower than the minimum. No column classes, no breakpoints —
   the track list is the whole layout. Knobs: --wel-grid-min, --wel-grid-gap.
   Subgrid row alignment (data-align="rows") is T-29. */
@layer layout {
  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--wel-grid-min, 20rem)), 1fr));
    gap: var(--wel-grid-gap, var(--wel-space-4));
    container-type: inline-size;
    container-name: layout;
  }
}

/* data-align="rows" (docs/06, T-29): every child spans its row's implicit
   tracks with subgrid, so headers, bodies, and footers align across cards
   regardless of content length. The named exception to "components never
   depend on parent layout". Gated: without subgrid, span 3 would interleave
   cards across three outer rows — inside @supports the whole rule drops and
   cards stay self-sized. */
@layer variants {
  @supports (grid-template-rows: subgrid) {
    .grid[data-align="rows"] > :where(*) {
      display: grid;
      grid-row: span 3;
      grid-template-rows: subgrid;
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .sidebar-layout — sidebar + content pair that wraps to stacked when the
   content's share would drop below its minimum (docs/06, T-23). Intrinsic:
   no breakpoint — the flex-basis/grow arithmetic decides from available
   space. First child is the sidebar; data-side="end" flips the roles so
   the sidebar sits after the content. Knobs: --wel-sidebar-width,
   --wel-sidebar-content-min (percentage of the pair), --wel-sidebar-gap. */
@layer layout {
  .sidebar-layout {
    display: flex;
    flex-wrap: wrap;
    gap: var(--wel-sidebar-gap, var(--wel-space-5));
    container-type: inline-size;
    container-name: layout;
  }

  /* Sidebar: fixed preferred width, may grow when wrapped to full row. */
  .sidebar-layout > :where(:first-child) {
    flex-grow: 1;
    flex-basis: var(--wel-sidebar-width, 20rem);
  }

  /* Content: grows greedily; wrapping triggers when it would be squeezed
     below its minimum share. */
  .sidebar-layout > :where(:last-child) {
    flex-grow: 999;
    flex-basis: 0;
    min-inline-size: var(--wel-sidebar-content-min, 50%);
  }
}

/* data-side="end": the sidebar is the last child (after the content in
   source order). Attribute rules live in `variants` per ADR-0010's layout
   budget (0-1-0, primitive class only). */
@layer variants {
  .sidebar-layout[data-side="end"] > :where(:first-child) {
    flex-grow: 999;
    flex-basis: 0;
    min-inline-size: var(--wel-sidebar-content-min, 50%);
  }

  .sidebar-layout[data-side="end"] > :where(:last-child) {
    flex-grow: 1;
    flex-basis: var(--wel-sidebar-width, 20rem);
    min-inline-size: auto;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .stack — vertical flow with consistent rhythm (docs/06, T-21). The reset
   strips element margins; the stack owns inter-sibling space instead —
   spacing is a layout decision, not an element default. Rhythm varies by
   nesting stacks, not per-pair margin exceptions. Establishes a queryable
   container per the doctrine (docs/06 §doctrine #3/#4). */
@layer layout {
  .stack {
    display: flex;
    flex-direction: column;
    gap: var(--wel-stack-gap, var(--wel-space-4));
    container-type: inline-size;
    container-name: layout;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .switcher — row of equals that switches to a column below a width
   threshold (docs/06, T-24). The (threshold - 100%) * 999 flex-basis trick:
   positive (huge) when the container is narrower than the threshold, so
   every item jumps to full width; negative (clamped to content) when wider.
   Knobs: --wel-switcher-threshold, --wel-switcher-gap; data-limit="2|3|4"
   caps how many items may share a row (a selector cannot read a custom
   property, so the limit is markup — docs/06). */
@layer layout {
  .switcher {
    display: flex;
    flex-wrap: wrap;
    gap: var(--wel-switcher-gap, var(--wel-space-4));
    container-type: inline-size;
    container-name: layout;
  }

  .switcher > :where(*) {
    flex-grow: 1;
    flex-basis: calc((var(--wel-switcher-threshold, 30rem) - 100%) * 999);
  }
}

/* data-limit: with more items than the limit, every item goes full width —
   a crowded row degrades to a clean column rather than a cramped squeeze. */
@layer variants {
  .switcher[data-limit="2"] > :where(:nth-last-child(n + 3), :nth-last-child(n + 3) ~ *),
  .switcher[data-limit="3"] > :where(:nth-last-child(n + 4), :nth-last-child(n + 4) ~ *),
  .switcher[data-limit="4"] > :where(:nth-last-child(n + 5), :nth-last-child(n + 5) ~ *) {
    flex-basis: 100%;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .accordion — expandable sections on <details>/<summary>
   (docs/components/accordion.md, T-42). The platform supplies disclosure
   semantics, Enter/Space toggling, and expanded-state announcement; Welkin
   ships zero accordion JS, ever. Exclusivity is <details name> (inert
   where unsupported — degrades to independent multi-open, nothing breaks).
   The open/close size animation stays instant: interpolate-size is
   Chromium-only (rechecked at T-42) and blocked per ADR-0012; the Enhanced
   tier here is a panel fade via ::details-content (Baseline Sept 2025). */
@layer components {
  .accordion {
    --wel-accordion-bg: var(--wel-color-surface);
    --wel-accordion-ink: var(--wel-color-ink);
    --wel-accordion-border: var(--wel-color-border);
    --wel-accordion-radius: var(--wel-radius-surface);
    --wel-accordion-summary-padding-block: var(--wel-space-3);
    --wel-accordion-summary-padding-inline: var(--wel-space-4);
    --wel-accordion-panel-padding: var(--wel-space-4);
    --wel-accordion-marker-color: var(--wel-color-ink-muted);

    background-color: var(--wel-accordion-bg);
    border: var(--wel-border-width) solid var(--wel-accordion-border);
    border-radius: var(--wel-accordion-radius);
    /* Hover tints must not poke past the rounded corners. */
    overflow: clip;
  }

  .accordion > :where(details + details) {
    border-block-start: var(--wel-border-width) solid var(--wel-accordion-border);
  }

  .accordion :where(summary) {
    display: flex;
    align-items: center;
    gap: var(--wel-space-3);
    padding-block: var(--wel-accordion-summary-padding-block);
    padding-inline: var(--wel-accordion-summary-padding-inline);
    min-block-size: 1.5rem;
    color: var(--wel-color-ink-muted);
    font-weight: 500;
    cursor: pointer;
    /* The UA triangle goes; the chevron below replaces it. */
    list-style: none;
    transition: color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease);
  }

  .accordion :where(summary)::-webkit-details-marker {
    display: none;
  }

  /* Marker: the select caret's gradient chevron (shared technique — the
     spec's open marker question), currentColor-free so it consumes the
     marker token; a real pseudo box, so it rotates through --wel-motion.
     Points inline-end closed, down open; :dir() mirrors the closed
     direction for RTL. */
  .accordion :where(summary)::before {
    content: "";
    inline-size: 0.6em;
    block-size: 0.6em;
    flex: none;
    background-image:
      linear-gradient(135deg, var(--wel-accordion-marker-color) calc(50% - 0.5px), transparent calc(50% + 0.5px)),
      linear-gradient(45deg, transparent calc(50% - 0.5px), var(--wel-accordion-marker-color) calc(50% + 0.5px));
    background-size: 50% 0.4em;
    background-position: 100% 60%, 0 60%;
    background-repeat: no-repeat;
    rotate: -90deg;
    transition: rotate calc(var(--wel-motion-duration-2) * var(--wel-motion)) var(--wel-motion-ease);
  }

  .accordion :where(summary:dir(rtl))::before {
    rotate: 90deg;
  }

  .accordion :where(details[open]) > :where(summary) {
    color: var(--wel-accordion-ink);
  }

  .accordion :where(details[open]) > :where(summary)::before {
    rotate: 0deg;
  }

  /* Panel spacing on direct children — works in every engine (the
     ::details-content box is Enhanced-only). */
  .accordion :where(details) > :where(:not(summary)) {
    padding-inline: var(--wel-accordion-panel-padding);
  }

  .accordion :where(details) > :where(:not(summary):last-child) {
    padding-block-end: var(--wel-accordion-panel-padding);
  }

  /* Enhanced: content fades in on open (opacity only, riding --wel-motion).
     The height change stays instant — interpolate-size is blocked at
     intake (see header comment); when it lands, the size transition joins
     this block. */
  @supports selector(::details-content) and (transition-behavior: allow-discrete) {
    .accordion :where(details)::details-content {
      opacity: 0;
      transition:
        opacity calc(var(--wel-motion-duration-2) * var(--wel-motion)) var(--wel-motion-ease),
        content-visibility calc(var(--wel-motion-duration-2) * var(--wel-motion)) allow-discrete;
    }

    .accordion :where(details[open])::details-content {
      opacity: 1;
    }
  }

  /* Tight containers: padding steps down one space token. */
  @container layout (inline-size < 20rem) {
    .accordion {
      --wel-accordion-summary-padding-inline: var(--wel-space-3);
      --wel-accordion-panel-padding: var(--wel-space-3);
    }
  }
}

@layer variants {
  /* Detached rounded blocks. */
  .accordion[data-variant="separated"] {
    display: flex;
    flex-direction: column;
    gap: var(--wel-space-2);
    background: none;
    border: none;
    border-radius: 0;
    overflow: visible;
  }

  .accordion[data-variant="separated"] > :where(details) {
    background-color: var(--wel-accordion-bg);
    border: var(--wel-border-width) solid var(--wel-accordion-border);
    border-radius: var(--wel-accordion-radius);
    overflow: clip;
  }

  .accordion[data-variant="separated"] > :where(details + details) {
    border-block-start: var(--wel-border-width) solid var(--wel-accordion-border);
  }

  /* Dividers only — for embedding in cards and sidebars. */
  .accordion[data-variant="flush"] {
    background: none;
    border: none;
    border-radius: 0;
  }
}

@layer states {
  @media (hover: hover) {
    .accordion :where(summary):hover {
      background-color: var(--wel-color-accent-tint);
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .alert — toned message surface (docs/components/alert-callout.md, T-33):
   callouts (static, authored) and alerts (dynamic, urgent). The live-region
   role is a usage decision, not a component style — only dynamically
   inserted urgent messages carry role="alert" (spec's 4.1.3 discipline).
   Body text stays body-inked; only title/icon/accent-border take the tone.
   Neutral accent is ink-muted, not border-strong as the token table
   suggests: the accent also paints the title, and border-strong is 3.3:1 —
   below AA for text (the a11y section wins). */
@layer components {
  .alert {
    --wel-alert-bg: var(--wel-color-surface-sunken);
    --wel-alert-ink: var(--wel-color-ink);
    --wel-alert-accent: var(--wel-color-ink-muted);
    --wel-alert-radius: var(--wel-radius-surface);
    --wel-alert-padding: var(--wel-space-4);
    --wel-alert-gap: var(--wel-space-3);
    --wel-alert-icon-size: 1.25em;

    /* Links keep their normal styling, but plain accent misses AA on the
       tints (4.29:1 on sunken) — so the alert retints the accent for its
       subtree to the tone colour (cross-component influence via tokens,
       docs/04) and re-derives the interaction shades, which otherwise
       stay frozen at their :root-computed values. */
    --wel-color-accent: var(--wel-color-info);
    --wel-color-accent-hover: oklch(from var(--wel-color-accent) calc(l - 0.06) c h);
    --wel-color-accent-active: oklch(from var(--wel-color-accent) calc(l - 0.1) c h);

    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--wel-space-2) var(--wel-alert-gap);
    padding: var(--wel-alert-padding);
    background-color: var(--wel-alert-bg);
    color: var(--wel-alert-ink);
    border: var(--wel-border-width) solid transparent;
    border-inline-start: var(--wel-border-width-strong) solid var(--wel-alert-accent);
    border-radius: var(--wel-alert-radius);
  }

  /* Content flows in the second column; the icon owns the first. */
  .alert > :where(*) {
    grid-column: 2;
    margin: 0;
  }

  .alert > :where(.alert-icon) {
    grid-column: 1;
    align-self: start;
    inline-size: var(--wel-alert-icon-size);
    block-size: var(--wel-alert-icon-size);
    /* Centre the icon on the title's first line: the icon top-aligns to
       the line box, but glyphs sit half a leading lower. */
    margin-block-start: calc((1em * var(--wel-text-leading-normal) - var(--wel-alert-icon-size)) / 2);
    color: var(--wel-alert-accent);
  }

  .alert :where(.alert-title) {
    color: var(--wel-alert-accent);
    font-weight: 600;
  }

  /* Narrow containers (docs/06 doctrine): the icon column drops and the
     icon rides the title line, keeping the text measure usable. */
  @container layout (inline-size < 24rem) {
    .alert {
      display: block;
    }

    .alert > :where(.alert-icon) {
      float: inline-start;
      margin-inline-end: var(--wel-space-2);
    }

    .alert > :where(* + *) {
      margin-block-start: var(--wel-space-2);
    }

    /* The element after the floated icon IS the first line — a top margin
       there pushes the title below the float. */
    .alert > :where(.alert-icon + *) {
      margin-block-start: 0;
    }
  }

  /* Forced colors: tints vanish; box + inline-start emphasis survive as
     CanvasText borders (the engine keeps var()-computed author colours,
     so map explicitly — same gap as .button). */
  @media (forced-colors: active) {
    .alert {
      background-color: Canvas;
      color: CanvasText;
      border-color: CanvasText;
    }

    .alert > :where(.alert-icon),
    .alert :where(.alert-title) {
      color: CanvasText;
    }
  }
}

@layer variants {
  .alert[data-tone="info"]    { --wel-alert-bg: var(--wel-color-info-tint);    --wel-alert-accent: var(--wel-color-info);    --wel-color-accent: var(--wel-color-info); }
  .alert[data-tone="success"] { --wel-alert-bg: var(--wel-color-success-tint); --wel-alert-accent: var(--wel-color-success); --wel-color-accent: var(--wel-color-success); }
  .alert[data-tone="warning"] { --wel-alert-bg: var(--wel-color-warning-tint); --wel-alert-accent: var(--wel-color-warning); --wel-color-accent: var(--wel-color-warning); }
  .alert[data-tone="danger"]  { --wel-alert-bg: var(--wel-color-danger-tint);  --wel-alert-accent: var(--wel-color-danger);  --wel-color-accent: var(--wel-color-danger); }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .badge / .tag — small labelling pair (docs/components/badge-tag.md,
   T-32). A badge is static inline information and never focusable; a tag
   is a categorisation the user may follow (link) or remove (dismiss
   button, host-app-owned behaviour). One data-tone axis on both, fed by
   the tone token sets — every pair is a checked docs/05 pairing. The tag
   border paints border-strong: the spec's a11y section requires 3:1
   against surface, which the decorative border token cannot hold.
   Hover shifts ink as well as bg — a tint darkened by the 05 formula
   alone drops the success pair below 4.5:1 in light scheme. */
@layer components {
  .badge {
    --wel-badge-bg: var(--wel-color-surface-sunken);
    --wel-badge-ink: var(--wel-color-ink-muted);
    --wel-badge-radius: var(--wel-radius-full);
    --wel-badge-padding-block: var(--wel-space-1);
    --wel-badge-padding-inline: var(--wel-space-2);

    display: inline-block;
    padding-block: var(--wel-badge-padding-block);
    padding-inline: var(--wel-badge-padding-inline);
    background-color: var(--wel-badge-bg);
    color: var(--wel-badge-ink);
    border: var(--wel-border-width) solid transparent;
    border-radius: var(--wel-badge-radius);
    font-size: var(--wel-text-size--1);
    line-height: var(--wel-text-leading-tight);
    font-weight: 500;
  }

  .tag {
    --wel-tag-bg: var(--wel-color-surface-sunken);
    --wel-tag-ink: var(--wel-color-ink-muted);
    --wel-tag-radius: var(--wel-radius-sm);
    --wel-tag-padding-block: var(--wel-space-1);
    --wel-tag-padding-inline: var(--wel-space-3);
    --wel-tag-border: var(--wel-color-border-strong);
    --wel-tag-max-inline-size: none;
    --wel-tag-bg-hover: oklch(from var(--wel-tag-bg) calc(l - 0.06) c h);
    --wel-tag-bg-active: oklch(from var(--wel-tag-bg) calc(l - 0.1) c h);
    --wel-tag-ink-hover: light-dark(
      oklch(from var(--wel-tag-ink) calc(l - 0.06) c h),
      oklch(from var(--wel-tag-ink) calc(l + 0.06) c h)
    );

    /* inline-block, not flex: text-overflow cannot ellipsize an anonymous
       flex item, and truncation (--wel-tag-max-inline-size) must work on
       plain/link tags. Dismissible tags switch to flex below — their
       button needs the alignment and they don't truncate. */
    display: inline-block;
    padding-block: var(--wel-tag-padding-block);
    padding-inline: var(--wel-tag-padding-inline);
    max-inline-size: var(--wel-tag-max-inline-size);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom; /* clipped inline-block otherwise misaligns baselines */
    background-color: var(--wel-tag-bg);
    color: var(--wel-tag-ink);
    border: var(--wel-border-width) solid var(--wel-tag-border);
    border-radius: var(--wel-tag-radius);
    font-size: var(--wel-text-size--1);
    line-height: var(--wel-text-leading-tight);
    text-decoration: none; /* link tags */
    transition:
      background-color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
      color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease);
  }

  .tag:where(:has(.tag-dismiss)) {
    display: inline-flex;
    align-items: center;
    gap: var(--wel-space-1);
  }

  /* Dismiss: a real button, ≥ 24px target (2.5.8) — the tag grows to
     accommodate it rather than the button shrinking. */
  .tag :where(.tag-dismiss) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: 1.5rem;
    min-block-size: 1.5rem;
    padding: 0;
    background: none;
    border: 0;
    border-radius: var(--wel-tag-radius);
    color: inherit;
    font: inherit;
  }

  .tag :where(.tag-dismiss) > :where(svg) {
    inline-size: 0.75em;
    block-size: 0.75em;
  }

  /* Forced colors: tone tints vanish and the boundary survives as a
     border. Colours are mapped explicitly — the engine does not force
     var()-computed author colours (same gap as .button). */
  @media (forced-colors: active) {
    .badge,
    .tag {
      background-color: Canvas;
      color: CanvasText;
      border-color: CanvasText;
    }

    .tag:where(a) {
      color: LinkText;
    }

    .tag :where(.tag-dismiss) {
      color: ButtonText;
    }
  }
}

@layer variants {
  .badge[data-tone="info"]    { --wel-badge-bg: var(--wel-color-info-tint);    --wel-badge-ink: var(--wel-color-info); }
  .badge[data-tone="success"] { --wel-badge-bg: var(--wel-color-success-tint); --wel-badge-ink: var(--wel-color-success); }
  .badge[data-tone="warning"] { --wel-badge-bg: var(--wel-color-warning-tint); --wel-badge-ink: var(--wel-color-warning); }
  .badge[data-tone="danger"]  { --wel-badge-bg: var(--wel-color-danger-tint);  --wel-badge-ink: var(--wel-color-danger); }

  .tag[data-tone="info"]    { --wel-tag-bg: var(--wel-color-info-tint);    --wel-tag-ink: var(--wel-color-info); }
  .tag[data-tone="success"] { --wel-tag-bg: var(--wel-color-success-tint); --wel-tag-ink: var(--wel-color-success); }
  .tag[data-tone="warning"] { --wel-tag-bg: var(--wel-color-warning-tint); --wel-tag-ink: var(--wel-color-warning); }
  .tag[data-tone="danger"]  { --wel-tag-bg: var(--wel-color-danger-tint);  --wel-tag-ink: var(--wel-color-danger); }
}

@layer states {
  @media (hover: hover) {
    .tag:where(a):hover {
      background-color: var(--wel-tag-bg-hover);
      color: var(--wel-tag-ink-hover);
    }
  }

  .tag:where(a):active {
    background-color: var(--wel-tag-bg-active);
    color: var(--wel-tag-ink-hover);
  }

  /* Selected filter tag: fills with the tone/accent colour; border weight
     carries the state under forced colors (never background-only). */
  .tag[aria-selected="true"],
  .tag[aria-pressed="true"] {
    background-color: var(--wel-tag-ink);
    color: var(--wel-color-surface);
    border-width: var(--wel-border-width-strong);
    border-color: var(--wel-tag-ink);
  }

  /* This layer beats the components-layer forced-colors mapping, so the
     selected fill needs its own: the system selection pair, plus the
     border-weight cue that is the not-background-only treatment. */
  @media (forced-colors: active) {
    .tag[aria-selected="true"],
    .tag[aria-pressed="true"] {
      background-color: SelectedItem;
      color: SelectedItemText;
      border-color: CanvasText;
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .breadcrumb — hierarchy trail (docs/components/breadcrumb.md, T-34).
   Separators are CSS-generated from a string token, never markup; with
   alternative-text syntax (Enhanced tier) they are declared empty for
   assistive tech. The current page is styled from [aria-current="page"]
   itself. The list wraps by default — hidden overflow is never the
   baseline; narrow containers engage per-item ellipsis caps instead. */
@layer components {
  .breadcrumb {
    --wel-breadcrumb-ink: var(--wel-color-ink-muted);
    --wel-breadcrumb-ink-current: var(--wel-color-ink);
    --wel-breadcrumb-gap: var(--wel-space-2);
    --wel-breadcrumb-separator: "/";
    --wel-breadcrumb-item-max: 12rem;
  }

  .breadcrumb > :where(ol) {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--wel-breadcrumb-gap);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .breadcrumb :where(li) {
    display: inline-flex;
    align-items: center;
    gap: var(--wel-breadcrumb-gap);
  }

  .breadcrumb :where(li + li)::before {
    content: var(--wel-breadcrumb-separator);
    color: var(--wel-color-ink-faint);
  }

  /* Alternative-text syntax guarantees screen readers ignore the glyph;
     without support the plain separator above stands (additive-only). */
  @supports (content: "a" / "b") {
    .breadcrumb :where(li + li)::before {
      content: var(--wel-breadcrumb-separator) / "";
    }
  }

  .breadcrumb :where(a) {
    display: inline-block;
    /* 2.5.8: pad the link target to ≥ 24px block-size. */
    padding-block: var(--wel-space-1);
    min-block-size: 1.5rem;
    color: var(--wel-breadcrumb-ink);
    text-decoration: none;
  }

  .breadcrumb :where(a[aria-current="page"]) {
    color: var(--wel-breadcrumb-ink-current);
  }

  /* Narrow containers: cap each crumb; the full text stays the link's
     accessible name — the ellipsis is visual only. */
  @container layout (inline-size < 30rem) {
    .breadcrumb :where(a) {
      max-inline-size: var(--wel-breadcrumb-item-max);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }

  @container layout (inline-size < 20rem) {
    .breadcrumb {
      --wel-breadcrumb-item-max: 8rem;
    }
  }

  /* Forced colors: the ink distinction collapses, so the current item is
     the one WITHOUT the underline the trail links receive. */
  @media (forced-colors: active) {
    .breadcrumb :where(a) {
      text-decoration: underline;
    }

    .breadcrumb :where(a[aria-current="page"]) {
      text-decoration: none;
    }
  }
}

@layer states {
  @media (hover: hover) {
    .breadcrumb :where(a):hover {
      color: var(--wel-breadcrumb-ink-current);
      text-decoration: underline;
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .button — the action trigger (docs/components/button.md, T-30). Pure
   CSS, Core tier. Variants/sizes are data-* axes (ADR-0001); every bg/ink
   pair is a guaranteed pairing from the docs/05 table in both schemes.
   Interaction shades derive from --wel-button-bg via the 05 formulas —
   themes retint every state by setting one token. Variants with a
   transparent bg point the -hover/-active tokens at tint pairings instead
   (a lightness offset from transparent is meaningless). */
@layer components {
  .button {
    --wel-button-bg: var(--wel-color-surface-sunken);
    --wel-button-ink: var(--wel-color-ink);
    --wel-button-border: transparent;
    --wel-button-radius: var(--wel-radius-control);
    --wel-button-padding-block: var(--wel-space-2);
    --wel-button-padding-inline: var(--wel-space-4);
    --wel-button-bg-hover: oklch(from var(--wel-button-bg) calc(l - 0.06) c h);
    --wel-button-bg-active: oklch(from var(--wel-button-bg) calc(l - 0.1) c h);
    --wel-button-ink-hover: var(--wel-button-ink);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--wel-space-2);
    padding-block: var(--wel-button-padding-block);
    padding-inline: var(--wel-button-padding-inline);
    /* WCAG 2.5.8: every size keeps a ≥ 24px block-size target. */
    min-block-size: 1.5rem;
    background-color: var(--wel-button-bg);
    color: var(--wel-button-ink);
    border: var(--wel-border-width) solid var(--wel-button-border);
    border-radius: var(--wel-button-radius);
    font: inherit;
    font-size: var(--wel-text-size-0);
    line-height: var(--wel-text-leading-tight);
    font-weight: 500;
    text-decoration: none; /* .button on <a> */
    transition:
      background-color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
      color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
      border-color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
      translate calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease);
  }

  .button > :where(svg) {
    inline-size: 1em;
    block-size: 1em;
    flex: none;
  }

  /* Forced colors: the engine forces plain author colours but not ones
     computed through var(), so map to the system button palette explicitly.
     Variant distinctions collapse by design (spec a11y section). */
  @media (forced-colors: active) {
    .button {
      background-color: ButtonFace;
      color: ButtonText;
      border-color: ButtonBorder;
    }

    .button:where(:disabled) {
      color: GrayText;
      border-color: GrayText;
    }
  }
}

@layer variants {
  .button[data-variant="primary"] {
    --wel-button-bg: var(--wel-color-accent);
    --wel-button-ink: var(--wel-color-accent-contrast);
  }

  .button[data-variant="secondary"] {
    --wel-button-bg: transparent;
    --wel-button-ink: var(--wel-color-ink);
    --wel-button-border: var(--wel-color-border-strong);
    --wel-button-bg-hover: var(--wel-color-accent-tint);
    --wel-button-bg-active: var(--wel-color-accent-tint);
    --wel-button-ink-hover: var(--wel-color-accent-hover);
  }

  .button[data-variant="ghost"] {
    --wel-button-bg: transparent;
    --wel-button-ink: var(--wel-color-accent);
    --wel-button-border: var(--wel-color-border-strong);
    --wel-button-bg-hover: var(--wel-color-accent-tint);
    --wel-button-bg-active: var(--wel-color-accent-tint);
    --wel-button-ink-hover: var(--wel-color-accent-hover);
  }

  .button[data-variant="danger"] {
    --wel-button-bg: var(--wel-color-danger);
    --wel-button-ink: var(--wel-color-surface);
  }

  .button[data-size="sm"] {
    --wel-button-padding-block: var(--wel-space-1);
    --wel-button-padding-inline: var(--wel-space-3);

    font-size: var(--wel-text-size--1);
  }

  .button[data-size="lg"] {
    --wel-button-padding-block: var(--wel-space-3);
    --wel-button-padding-inline: var(--wel-space-5);

    font-size: var(--wel-text-size-1);
  }
}

@layer states {
  @media (hover: hover) {
    .button:hover {
      background-color: var(--wel-button-bg-hover);
      color: var(--wel-button-ink-hover);
    }
  }

  .button:active {
    background-color: var(--wel-button-bg-active);
    color: var(--wel-button-ink-hover);
    /* 1px press, zeroed under reduced motion with the rest of the system. */
    translate: 0 calc(1px * var(--wel-motion));
  }

  /* Busy: inline spinner takes the leading icon slot, label stays visible.
     The animation duration rides the --wel-motion multiplier like every
     other duration — frozen ring under reduced motion, aria-busy still
     announces. */
  .button[aria-busy="true"] {
    pointer-events: none;
  }

  .button[aria-busy="true"]::before {
    content: "";
    inline-size: 1em;
    block-size: 1em;
    flex: none;
    border: 0.125em solid;
    border-block-start-color: transparent;
    border-radius: var(--wel-radius-full);
    animation: wel-button-spin calc(var(--wel-motion-duration-4) * 2 * var(--wel-motion)) linear infinite;
  }

  .button[aria-busy="true"] > :where(svg:first-of-type) {
    display: none;
  }

  /* Disabled last in the layer so it beats hover/active at equal
     specificity. Reduced-contrast pair is the documented WCAG 1.4.3
     exemption for disabled controls. */
  .button:disabled {
    background-color: var(--wel-color-surface-sunken);
    color: var(--wel-color-ink-faint);
    border-color: transparent;
    translate: none;
  }
}

@keyframes wel-button-spin {
  to {
    rotate: 1turn;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .card — bounded content surface (docs/components/card.md, T-31). Pure
   CSS. Parts are direct children (.card-media/-header/-body/-footer) so
   subgrid alignment via .grid[data-align="rows"] can take the card's rows —
   the layer order lets the primitive's display:grid win over the flex flow
   here. Padding is applied per part, not on the root, so media bleeds to
   the card edge. The stretched .card-link makes the whole card the
   heading-link's target without wrapping the card in <a>.
   Outlined paints border-strong: the spec's a11y section requires 3:1
   against both card and page surface (checked pairings), which the
   decorative border token cannot hold. */
@layer components {
  .card {
    --wel-card-bg: var(--wel-color-surface-raised);
    --wel-card-ink: var(--wel-color-ink);
    --wel-card-radius: var(--wel-radius-surface);
    --wel-card-padding: var(--wel-space-5);
    --wel-card-gap: var(--wel-space-3);
    --wel-card-border: var(--wel-color-border-strong);
    --wel-card-shadow: var(--wel-shadow-1);
    --wel-card-shadow-hover: var(--wel-shadow-2);
    --wel-card-border-hover: transparent;

    position: relative; /* anchors the stretched link */
    display: flex;
    flex-direction: column;
    gap: var(--wel-card-gap);
    overflow: clip; /* media inherits the outer radius */
    background-color: var(--wel-card-bg);
    color: var(--wel-card-ink);
    border: var(--wel-border-width) solid transparent;
    border-radius: var(--wel-card-radius);
    box-shadow: var(--wel-card-shadow);
    transition:
      box-shadow calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
      border-color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease);
  }

  /* Per-part padding; media is the exception and bleeds edge to edge. */
  .card > :where(.card-header, .card-body, .card-footer) {
    padding-inline: var(--wel-card-padding);
  }

  .card > :where(:first-child):where(:not(.card-media)) {
    padding-block-start: var(--wel-card-padding);
  }

  .card > :where(:last-child):where(:not(.card-media)) {
    padding-block-end: var(--wel-card-padding);
  }

  .card > :where(.card-media) > :where(img, video, .frame) {
    inline-size: 100%;
    display: block;
  }

  /* Stretched link: the card becomes the heading-link's hit area; the
     accessible name stays the heading text (docs/components/card.md). */
  .card :where(.card-link)::after {
    content: "";
    position: absolute;
    inset: 0;
  }

  /* Secondary interactive elements sit above the stretch. */
  .card :where(.card-footer) :where(a, button) {
    position: relative;
  }

  /* Forced colors: surface/shadow distinctions collapse, so every variant
     renders a border — the card boundary survives (spec a11y section). */
  @media (forced-colors: active) {
    .card {
      border-color: CanvasText;
    }
  }
}

@layer variants {
  .card[data-variant="outlined"] {
    --wel-card-shadow: none;
    --wel-card-shadow-hover: none;
    /* "border strengthens" must work in both schemes: ink-muted is darker
       than border-strong in light and lighter in dark — stronger in both. */
    --wel-card-border-hover: var(--wel-color-ink-muted);

    border-color: var(--wel-card-border);
  }

  .card[data-variant="plain"] {
    --wel-card-bg: transparent;
    --wel-card-shadow: none;
    --wel-card-shadow-hover: none;
  }
}

@layer states {
  /* Interactive states exist only on clickable cards (those with a
     .card-link); :where() keeps the :has() test weightless. */
  @media (hover: hover) {
    .card:where(:has(.card-link)):hover {
      box-shadow: var(--wel-card-shadow-hover);
      border-color: var(--wel-card-border-hover);
    }
  }

  /* Focus ring is drawn at the card boundary; the stretched link's own
     ring is suppressed in its favour. */
  .card:where(:has(.card-link:focus-visible)) {
    outline: var(--wel-focus-ring-width) solid var(--wel-focus-ring-color);
    outline-offset: var(--wel-focus-ring-offset);
  }

  .card :where(.card-link:focus-visible) {
    outline: none;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .carousel — scroll-snap slide rail (docs/components/carousel.md, T-46).
   The browser's scroll machinery IS the widget: touch, momentum, wheel,
   scrollbar, keyboard (the ul is the focusable scroller), RTL for free.
   No autoplay in v1 by scope decision (2.2.2). ::scroll-marker /
   ::scroll-button dots+buttons are spec-defined but Chromium-only —
   implementation-blocked per ADR-0012 / doc 03 audit note; they enter this
   file at intake. Until then hosts needing pointer-visible controls in
   fallback browsers use the carousel-buttons.js module (Phase 5, 2.5.7). */
@layer components {
  .carousel {
    --wel-carousel-gap: var(--wel-space-4);
    --wel-carousel-slide-size: 100%;
    --wel-carousel-padding: var(--wel-space-gutter);
    --wel-carousel-marker: var(--wel-color-border-strong);
    --wel-carousel-marker-current: var(--wel-color-accent);
    --wel-carousel-button-bg: var(--wel-color-surface-raised);
    --wel-carousel-button-ink: var(--wel-color-ink);
  }

  /* overflow-x / overscroll-behavior-x: permitted physical exception
     (ADR-0009) until the logical overflow-inline forms graduate; the snap
     axis itself is the logical `inline` keyword. */
  .carousel :where(.carousel-track) {
    display: flex;
    gap: var(--wel-carousel-gap);
    padding-inline: var(--wel-carousel-padding);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: inline mandatory;
    scroll-padding-inline: var(--wel-carousel-padding);
  }

  .carousel :where(.carousel-track > *) {
    flex: 0 0 var(--wel-carousel-slide-size);
    scroll-snap-align: start;
  }

  /* Enhanced: scroll-progress bar under the scroller. Gated off under
     reduced motion per the spec's preference matrix (unlike the sticky
     shadows, it is a moving element). timeline-scope lifts the scroller's
     timeline to the root so the ::after bar can consume it. */
  @supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
      .carousel {
        timeline-scope: --wel-carousel-scroll;
      }

      .carousel :where(.carousel-track) {
        scroll-timeline: --wel-carousel-scroll inline;
      }

      .carousel::after {
        content: "";
        display: block;
        /* px hairline class (ADR-0008). */
        block-size: 3px;
        margin-block-start: var(--wel-space-2);
        margin-inline: var(--wel-carousel-padding);
        inline-size: 0;
        background-color: var(--wel-color-accent);
        border-radius: var(--wel-radius-full);
        animation: wel-carousel-progress linear both;
        animation-timeline: --wel-carousel-scroll;
      }
    }
  }
}

@layer variants {
  /* Gallery snap: one centred slide. */
  .carousel[data-align="center"] :where(.carousel-track > *) {
    scroll-snap-align: center;
  }
}

@keyframes wel-carousel-progress {
  to {
    inline-size: calc(100% - 2 * var(--wel-carousel-padding));
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* wel-combobox — filtered suggestion listbox on a native input
   (docs/components/combobox.md, T-48). The input itself is styled by
   form-controls; everything here is the generated listbox, keyed to the
   roles/attributes wel-combobox.js writes. Deliberately NOT a top-layer
   popover: plain absolute positioning inside the relative root works in
   every engine, needs no anchor positioning, and stays inside an open
   dialog's top-layer box. Pre-upgrade/no-JS the native datalist UI
   renders — nothing here to style, nothing to hide. */
@layer components {
  wel-combobox {
    --wel-combobox-bg: var(--wel-color-surface-raised);
    --wel-combobox-ink: var(--wel-color-ink);
    --wel-combobox-border: var(--wel-color-border);
    --wel-combobox-radius: var(--wel-radius-surface);
    --wel-combobox-shadow: var(--wel-shadow-3);
    --wel-combobox-active-bg: var(--wel-color-accent-tint);
    --wel-combobox-max-block-size: 18rem;

    display: block;
    position: relative;
  }

  wel-combobox [role="listbox"] {
    position: absolute;
    inset-block-start: calc(100% + var(--wel-space-1));
    inset-inline: 0;
    z-index: 1;
    max-block-size: var(--wel-combobox-max-block-size);
    overflow-y: auto;
    margin: 0;
    padding: var(--wel-space-1);
    list-style: none;
    background-color: var(--wel-combobox-bg);
    color: var(--wel-combobox-ink);
    /* Hairline normally; THE panel edge under forced colors. */
    border: var(--wel-border-width) solid var(--wel-combobox-border);
    border-radius: var(--wel-combobox-radius);
    box-shadow: var(--wel-combobox-shadow);
  }

  /* Options pad themselves to ~24px+ targets (2.5.8). */
  wel-combobox [role="option"] {
    padding-block: var(--wel-space-2);
    padding-inline: var(--wel-space-2);
    min-block-size: 1.5rem;
    border-radius: var(--wel-radius-sm);
    cursor: pointer;
  }
}

@layer states {
  @media (hover: hover) {
    wel-combobox [role="option"]:hover {
      background-color: var(--wel-combobox-active-bg);
    }
  }

  /* Virtual focus (aria-activedescendant) — the only "focus ring" the
     option gets, so the fill must survive forced colors: the same
     Highlight/HighlightText convention the UA select uses. */
  wel-combobox [role="option"][aria-selected="true"] {
    background-color: var(--wel-combobox-active-bg);
  }

  @media (forced-colors: active) {
    wel-combobox [role="option"][aria-selected="true"] {
      background-color: Highlight;
      color: HighlightText;
      forced-color-adjust: none;
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .dialog — the interruption surface on native <dialog>
   (docs/components/dialog.md, T-40). showModal() supplies focus trap,
   background inert, Esc, top layer, aria-modal — zero focus-management JS.
   Closing is declarative (form method="dialog"); opening is the one
   platform gap: Invoker Commands are pre-Baseline (ADR-0012), so the Core
   path is the documented 3-line script / wel-dialog module. closedby="any"
   light dismiss is likewise withheld from shipped markup at implementation
   (Safari unshipped, checked at T-40) — the mandatory close button already
   covers every user. */
@layer components {
  .dialog {
    --wel-dialog-bg: var(--wel-color-surface-raised);
    --wel-dialog-ink: var(--wel-color-ink);
    --wel-dialog-radius: var(--wel-radius-surface);
    --wel-dialog-padding: var(--wel-space-5);
    --wel-dialog-shadow: var(--wel-shadow-4);
    --wel-dialog-max-inline-size: 36rem;
    /* A scrim always DARKENS — ink-based mixing inverts in dark mode (ink
       is near-white there, so the "dim" lit the page up; T-40 review). The
       dark scheme needs a deeper, stronger scrim to read over an already
       dark page. */
    --wel-dialog-backdrop: light-dark(oklch(21% 0.01 250 / 0.45), oklch(8% 0.005 250 / 0.6));

    inline-size: min(var(--wel-dialog-max-inline-size), calc(100dvw - 2 * var(--wel-space-gutter)));
    max-block-size: calc(100dvh - 2 * var(--wel-space-gutter));
    padding: var(--wel-dialog-padding);
    background-color: var(--wel-dialog-bg);
    color: var(--wel-dialog-ink);
    /* Hairline normally; THE panel edge under forced colors, where the
       shadow is stripped and backdrop dimming may be suppressed. */
    border: var(--wel-border-width) solid var(--wel-color-border);
    border-radius: var(--wel-dialog-radius);
    box-shadow: var(--wel-dialog-shadow);
    overflow: auto;
    overscroll-behavior: contain;
  }

  /* Custom-property inheritance into ::backdrop is Baseline 2024, pending
     the graduation audit — hence the literal fallback (ink at 45% ≈ the
     token's light-scheme value). */
  .dialog::backdrop {
    background-color: var(--wel-dialog-backdrop, oklch(21% 0.01 250 / 0.45));
  }

  .dialog > :where(header) {
    display: flex;
    align-items: start;
    justify-content: space-between;
    gap: var(--wel-space-3);
    margin-block-end: var(--wel-space-3);
  }

  .dialog > :where(header) :where(h1, h2, h3) {
    font-size: var(--wel-text-size-2);
    line-height: var(--wel-text-leading-tight);
  }

  .dialog > :where(footer) {
    margin-block-start: var(--wel-space-4);
  }

  /* method="dialog" close/action forms are wiring, not layout boxes. */
  .dialog :where(form[method="dialog"]) {
    display: contents;
  }

  /* Non-modal (.show() or server-rendered [open]): genuinely in flow — the
     UA default is position: absolute, which overlays following content. No
     backdrop, no trap: a panel, not an interruption. */
  .dialog:where(:not(:modal)) {
    position: static;
    margin: 0;
    box-shadow: var(--wel-shadow-1);
  }

  /* Enhanced: fade/scale entry and exit on panel and backdrop. display +
     overlay in the list so the exit plays before the element leaves the
     top layer (engines without `overlay` may snap the exit; entry is
     unaffected). All durations ride --wel-motion — instant under reduced
     motion, identical to Core. */
  @supports (transition-behavior: allow-discrete) {
    .dialog {
      opacity: 0;
      scale: 0.96;
      transition:
        opacity calc(var(--wel-motion-duration-2) * var(--wel-motion)) var(--wel-motion-ease),
        scale calc(var(--wel-motion-duration-2) * var(--wel-motion)) var(--wel-motion-ease),
        display calc(var(--wel-motion-duration-2) * var(--wel-motion)) allow-discrete,
        overlay calc(var(--wel-motion-duration-2) * var(--wel-motion)) allow-discrete;
    }

    .dialog:where([open]) {
      opacity: 1;
      scale: 1;
    }

    .dialog::backdrop {
      opacity: 0;
      transition:
        opacity calc(var(--wel-motion-duration-2) * var(--wel-motion)) var(--wel-motion-ease),
        display calc(var(--wel-motion-duration-2) * var(--wel-motion)) allow-discrete,
        overlay calc(var(--wel-motion-duration-2) * var(--wel-motion)) allow-discrete;
    }

    .dialog:where([open])::backdrop {
      opacity: 1;
    }

    @starting-style {
      .dialog:where([open]) {
        opacity: 0;
        scale: 0.96;
      }

      .dialog:where([open])::backdrop {
        opacity: 0;
      }
    }
  }
}

@layer variants {
  .dialog[data-size="sm"] {
    --wel-dialog-max-inline-size: 24rem;
  }

  .dialog[data-size="lg"] {
    --wel-dialog-max-inline-size: 48rem;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .field — form control + label + hint + error composition, and the styled
   native controls inside it (docs/components/form-controls.md, T-44).
   Validation is platform state: :user-invalid (waits for interaction — bare
   :invalid would paint errors at page load) plus [aria-invalid="true"] as
   the server-rendered twin (the CakePHP FormHelper contract). The .error
   element ships in markup, wired into aria-describedby, and CSS reveals it.
   No JS: native constraint validation is the engine. */
@layer components {
  .field {
    --wel-field-gap: var(--wel-space-1);
    --wel-input-bg: var(--wel-color-surface);
    --wel-input-ink: var(--wel-color-ink);
    --wel-input-border: var(--wel-color-border-strong);
    --wel-input-radius: var(--wel-radius-control);
    --wel-input-padding-block: var(--wel-space-2);
    --wel-input-padding-inline: var(--wel-space-3);
    --wel-control-accent: var(--wel-color-accent);
    --wel-field-hint-ink: var(--wel-color-ink-muted);
    --wel-field-error-ink: var(--wel-color-danger);

    display: flex;
    flex-direction: column;
    align-items: start;
    gap: var(--wel-field-gap);
  }

  .field > :where(label),
  .field > :where(legend) {
    font-weight: 500;
  }

  /* Box styling applies to text-like controls only; checks/radios/range/
     color/file keep native form. Buttons never sit in a field. */
  .field :where(input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"])),
  .field :where(textarea) {
    inline-size: 100%;
    padding-block: var(--wel-input-padding-block);
    padding-inline: var(--wel-input-padding-inline);
    background-color: var(--wel-input-bg);
    color: var(--wel-input-ink);
    border: var(--wel-border-width) solid var(--wel-input-border);
    border-radius: var(--wel-input-radius);
    font: inherit;
    line-height: var(--wel-text-leading-tight);
    transition: border-color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease);
  }

  .field :where(textarea) {
    resize: vertical;
  }

  /* Enhanced (03 contract row): textarea grows with content to a cap;
     fallback is the fixed box with its native resize handle above. */
  @supports (field-sizing: content) {
    .field :where(textarea) {
      field-sizing: content;
      min-block-size: 2lh;
      max-block-size: var(--wel-textarea-max-block, 12lh);
    }
  }

  /* Checks and radios: native rendering retinted through the component
     token (accent-color), sized to the 24px WCAG 2.5.8 floor at every
     data-size — the default shipped path; dark mode and forced colors stay
     the UA's. */
  .field :where(input[type="checkbox"], input[type="radio"]) {
    inline-size: 1.5rem;
    block-size: 1.5rem;
    accent-color: var(--wel-control-accent);
    flex: none;
  }

  /* Check/radio/switch fields put the label after the control on one line.
     :where() keeps :has() out of the specificity budget (0-1-0). */
  .field:where(:has(> input[type="checkbox"], > input[type="radio"])) {
    flex-direction: row;
    align-items: center;
    gap: var(--wel-space-2);
  }

  .field:where(:has(> input[type="checkbox"], > input[type="radio"])) > :where(label) {
    font-weight: 400;
  }

  /* Radio/checkbox groups: fieldset.field wraps single-control fields. */
  .field:where(fieldset) {
    border: 0;
    padding: 0;
    margin: 0;
  }

  .field :where(.hint) {
    color: var(--wel-field-hint-ink);
    font-size: var(--wel-text-size--1);
  }

  /* Error text is present in markup (aria-describedby association is
     static) and revealed only when the control is invalid — client state
     (:user-invalid) or the server-rendered path ([aria-invalid="true"]). */
  .field :where(.error) {
    display: none;
    color: var(--wel-field-error-ink);
    font-size: var(--wel-text-size--1);
    font-weight: 500;
  }

  .field:where(:has(:user-invalid, [aria-invalid="true"])) :where(.error) {
    display: block;
  }

  .field:where(:has(:user-invalid, [aria-invalid="true"])) > :where(label, legend) {
    color: var(--wel-field-error-ink);
  }

  .field:where(:has(:disabled)) > :where(label, legend) {
    color: var(--wel-color-ink-faint);
  }

  /* .switch — a checkbox with switch semantics (role="switch") and switch
     drawing: appearance: none track, thumb as a radial-gradient layer so no
     pseudo-element is needed on the replaced element (unreliable there).
     Thumb travel rides background-position through --wel-motion. */
  .field :where(input.switch) {
    appearance: none;
    inline-size: 2.75rem;
    block-size: 1.5rem;
    border: var(--wel-border-width) solid var(--wel-input-border);
    border-radius: var(--wel-radius-full);
    background-color: var(--wel-color-surface-sunken);
    background-image: radial-gradient(circle closest-side, var(--wel-color-ink-muted) 92%, transparent);
    background-size: 1.125rem 1.125rem;
    background-position: 0.1875rem 50%;
    background-repeat: no-repeat;
    transition:
      background-position calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
      background-color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease);
  }

  /* background-position has no flow-relative syntax — permitted physical
     exception (ADR-0009): the resting thumb sits at the inline start. */
  .field :where(input.switch:dir(rtl)) {
    background-position: calc(100% - 0.1875rem) 50%;
  }

  /* Forced colors: gradients and backgrounds are erased, so the drawn thumb
     cannot survive — hand the control back to the UA's checkbox rendering
     (role="switch" still announces correctly). Same wholesale-revert policy
     as .select. */
  @media (forced-colors: active) {
    .field :where(input.switch) {
      appearance: auto;
      background-image: none;
    }
  }
}

@layer variants {
  .field[data-size="sm"] {
    --wel-input-padding-block: var(--wel-space-1);
    --wel-input-padding-inline: var(--wel-space-2);

    font-size: var(--wel-text-size--1);
  }

  .field[data-size="lg"] {
    --wel-input-padding-block: var(--wel-space-3);
    --wel-input-padding-inline: var(--wel-space-4);

    font-size: var(--wel-text-size-1);
  }
}

@layer states {
  @media (hover: hover) {
    .field :where(input, textarea):hover {
      border-color: var(--wel-color-ink-muted);
    }
  }

  /* Focus ring is the single global :focus-visible rule in base. */

  /* Read-only: sunken but bordered — distinguishable from disabled. Text
     controls only (checks/radios are always :read-only by definition). */
  .field :where(input:not([type="checkbox"], [type="radio"]), textarea):read-only {
    background-color: var(--wel-color-surface-sunken);
  }

  .field :where(input, textarea):user-invalid,
  .field :where(input, textarea)[aria-invalid="true"] {
    border-color: var(--wel-color-danger);
  }

  /* Valid-state affirmation is opt-in (data-show-valid on the <form> or
     any ancestor): green borders everywhere by default read as noise. A
     server-rendered error outranks client-side validity — the server knows
     things the constraint API can't (e.g. username taken). */
  :where([data-show-valid]) .field :where(input, textarea):user-valid:where(:not([aria-invalid="true"])) {
    border-color: var(--wel-color-success);
  }

  .field :where(input.switch):checked {
    background-color: var(--wel-control-accent);
    background-image: radial-gradient(circle closest-side, var(--wel-color-accent-contrast) 92%, transparent);
    background-position: calc(100% - 0.1875rem) 50%;
    border-color: var(--wel-control-accent);
  }

  .field :where(input.switch:dir(rtl)):checked {
    background-position: 0.1875rem 50%;
  }

  /* Disabled last in the layer so it beats hover at equal specificity;
     reduced-contrast pair is the documented WCAG 1.4.3 exemption. */
  .field :where(input, textarea):disabled {
    background-color: var(--wel-color-surface-sunken);
    color: var(--wel-color-ink-faint);
    border-color: var(--wel-color-border);
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .navbar — page-level navigation (docs/components/navbar.md, T-38).
   Zero JavaScript: the collapsed menu is the Popover API (popover +
   popovertarget — toggle, Esc, light dismiss, and aria-expanded sync are
   all native). ONE <ul> is both the inline link row and the popover
   panel: the UA's [popover] display:none loses to our expanded-state
   container-query display override. Collapse is driven by the `page`
   NAMED container (docs/06) — no ancestor page container means the query
   never matches and the navbar stays collapsed-safe. */
@layer components {
  .navbar {
    --wel-navbar-bg: var(--wel-color-surface-raised);
    --wel-navbar-ink: var(--wel-color-ink);
    --wel-navbar-padding-block: var(--wel-space-2);
    --wel-navbar-padding-inline: var(--wel-space-gutter);
    --wel-navbar-gap: var(--wel-space-4);
    --wel-navbar-block-size: 3.5rem;
    --wel-navbar-panel-bg: var(--wel-navbar-bg);
    --wel-navbar-border: var(--wel-color-border);

    background-color: var(--wel-navbar-bg);
    color: var(--wel-navbar-ink);
    border-block-end: var(--wel-border-width) solid var(--wel-navbar-border);
  }

  .navbar > :where(nav) {
    display: flex;
    align-items: center;
    gap: var(--wel-navbar-gap);
    min-block-size: var(--wel-navbar-block-size);
    padding-block: var(--wel-navbar-padding-block);
    padding-inline: var(--wel-navbar-padding-inline);
  }

  .navbar :where(.navbar-brand) {
    color: var(--wel-navbar-ink);
    font-weight: 600;
    text-decoration: none;
  }

  .navbar :where(.navbar-menu-button) {
    display: inline-flex;
    align-items: center;
    gap: var(--wel-space-1);
    margin-inline-start: auto;
    min-block-size: 1.5rem;
    padding-block: var(--wel-space-1);
    padding-inline: var(--wel-space-3);
    background: none;
    border: var(--wel-border-width) solid var(--wel-color-border-strong);
    border-radius: var(--wel-radius-control);
    color: var(--wel-navbar-ink);
    font: inherit;
  }

  .navbar :where(.navbar-menu-button) > :where(svg) {
    inline-size: 1em;
    block-size: 1em;
  }

  /* Collapsed panel (the popover, when open): pinned below the bar. Core
     placement assumes the bar's contract position at viewport top. */
  .navbar :where(.navbar-menu) {
    position: fixed;
    inset-block-start: var(--wel-navbar-block-size);
    inset-inline: 0;
    inline-size: auto;
    margin: 0;
    padding: var(--wel-space-3);
    list-style: none;
    background-color: var(--wel-navbar-panel-bg);
    color: var(--wel-navbar-ink);
    border: none;
    border-block-end: var(--wel-border-width) solid var(--wel-navbar-border);
    box-shadow: var(--wel-shadow-2);
  }

  .navbar :where(.navbar-menu li + li) {
    margin-block-start: var(--wel-space-1);
  }

  /* Panel links padded to ~44px targets. */
  .navbar :where(.navbar-menu a) {
    display: block;
    padding-block: var(--wel-space-2);
    padding-inline: var(--wel-space-2);
    color: var(--wel-color-ink-muted);
    text-decoration: none;
    border-radius: var(--wel-radius-sm);
  }

  /* The actions part lays out its own children — do NOT compose it with
     .cluster: a layout primitive's inline-size containment collapses it
     to zero width as an auto-sized flex item, spilling its contents past
     the bar edge. */
  .navbar :where(.navbar-actions) {
    display: flex;
    align-items: center;
    gap: var(--wel-space-2);
    margin-inline-start: auto;
  }

  /* When the menu-button is visible it owns the auto margin. */
  .navbar :where(.navbar-menu-button) ~ :where(.navbar-actions) {
    margin-inline-start: 0;
  }

  /* Expanded: the menu joins the bar inline; author display beats the UA
     popover display:none. 48rem is the component default. */
  @container page (inline-size >= 48rem) {
    .navbar :where(.navbar-menu) {
      display: flex;
      position: static;
      gap: var(--wel-space-2);
      padding: 0;
      background: none;
      border: none;
      box-shadow: none;
    }

    .navbar :where(.navbar-menu li + li) {
      margin-block-start: 0;
    }

    .navbar :where(.navbar-menu a) {
      display: inline-block;
      padding-block: var(--wel-space-1);
      min-block-size: 1.5rem;
    }

    .navbar :where(.navbar-menu-button) {
      display: none;
    }
  }

  /* Enhanced: panel tethers to its button — correct even off viewport-top. */
  @supports (anchor-name: --wel-a) {
    .navbar :where(.navbar-menu-button) {
      anchor-name: --wel-navbar-button;
    }

    .navbar :where(.navbar-menu) {
      position-anchor: --wel-navbar-button;
      inset-block-start: calc(anchor(end) + var(--wel-space-2));
    }
  }

  /* Enhanced: entry fade/slide (exit stays instant — a closed-state
     opacity would leak into the expanded inline arrangement). */
  @supports (transition-behavior: allow-discrete) {
    .navbar :where(.navbar-menu:popover-open) {
      transition:
        opacity calc(var(--wel-motion-duration-2) * var(--wel-motion)) var(--wel-motion-ease),
        translate calc(var(--wel-motion-duration-2) * var(--wel-motion)) var(--wel-motion-ease);
    }

    @starting-style {
      .navbar :where(.navbar-menu:popover-open) {
        opacity: 0;
        translate: 0 -0.5rem;
      }
    }
  }

  /* Forced colors: bg/shadow strip; the block-end border keeps the bar
     delineated and the current-page bar survives as a border. */
  @media (forced-colors: active) {
    .navbar {
      border-block-end-color: CanvasText;
    }

    .navbar :where(.navbar-menu a) {
      color: LinkText;
    }
  }
}

@layer variants {
  .navbar[data-sticky] {
    position: sticky;
    inset-block-start: 0;
    z-index: 10;
    /* shadow-2: elevation-1 does not read on dark surfaces (T-101). */
    box-shadow: var(--wel-shadow-2);
  }

  /* Enhanced: the sticky shadow appears only once the page has scrolled. */
  @supports (animation-timeline: scroll()) {
    .navbar[data-sticky] {
      animation: wel-navbar-shadow linear both;
      animation-timeline: scroll();
      animation-range: 0 6rem;
    }
  }
}

@layer states {
  @media (hover: hover) {
    .navbar :where(.navbar-menu a):hover {
      color: var(--wel-color-accent-hover);
    }
  }

  .navbar :where(.navbar-menu a):active {
    color: var(--wel-color-accent-active);
  }

  /* Current page: accent ink AND a bar — never colour alone. */
  .navbar :where(.navbar-menu a[aria-current="page"]) {
    color: var(--wel-color-accent);
    border-block-end: var(--wel-border-width-strong) solid var(--wel-color-accent);
  }

  /* The :has() trick: parent styled by child state, no JS classes. */
  .navbar:where(:has(.navbar-menu:popover-open)) {
    /* One tier above the sticky rest shadow so the raise stays visible. */
    box-shadow: var(--wel-shadow-3);
  }

  .navbar:where(:has(.navbar-menu:popover-open)) :where(.navbar-menu-button) {
    background-color: var(--wel-color-accent-tint);
  }
}

@keyframes wel-navbar-shadow {
  from {
    box-shadow: none;
  }

  to {
    box-shadow: var(--wel-shadow-2);
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .pagination — paged-collection navigation (docs/components/pagination.md,
   T-39). Links in a nav, never buttons: every page is a URL. Current page
   stays a link, styled from aria-current itself. No dead links — page 1
   omits Previous rather than faking disabled. Below 26rem containers the
   numbered links drop (display:none — AT sees what sighted users see),
   leaving a Previous / current / Next stepper. */
@layer components {
  .pagination {
    --wel-pagination-gap: var(--wel-space-1);
    --wel-pagination-item-ink: var(--wel-color-ink-muted);
    --wel-pagination-current-bg: var(--wel-color-accent);
    --wel-pagination-current-ink: var(--wel-color-accent-contrast);
    --wel-pagination-radius: var(--wel-radius-control);
    --wel-pagination-item-size: 2.5rem;
  }

  .pagination > :where(ol) {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--wel-pagination-gap);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .pagination :where(a) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 2.5.8 floor: the token keeps every item ≥ 24px even resized. */
    min-inline-size: var(--wel-pagination-item-size);
    min-block-size: var(--wel-pagination-item-size);
    padding-inline: var(--wel-space-2);
    color: var(--wel-pagination-item-ink);
    text-decoration: none;
    border-radius: var(--wel-pagination-radius);
    transition:
      background-color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
      color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease);
  }

  /* Ellipsis items are presentational (aria-hidden in markup). */
  .pagination :where(li[aria-hidden="true"]) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: var(--wel-pagination-item-size);
    color: var(--wel-color-ink-faint);
  }

  /* Compact stepper in narrow containers: only prev/next (rel) and the
     current page survive. */
  @container layout (inline-size < 26rem) {
    .pagination :where(li:not(:has(a[rel])):not(:has(a[aria-current]))) {
      display: none;
    }
  }

  @media (forced-colors: active) {
    .pagination :where(a) {
      color: LinkText;
      text-decoration: underline;
    }
  }
}

@layer variants {
  .pagination[data-size="sm"] {
    --wel-pagination-item-size: 2rem;

    font-size: var(--wel-text-size--1);
  }
}

@layer states {
  @media (hover: hover) {
    .pagination :where(a):hover {
      background-color: var(--wel-color-accent-tint);
      color: var(--wel-color-ink);
    }
  }

  .pagination :where(a):active {
    background-color: oklch(from var(--wel-color-accent-tint) calc(l - 0.04) c h);
    color: var(--wel-color-ink);
  }

  /* Current page — after hover/active so it wins at equal specificity. */
  .pagination :where(a[aria-current="page"]) {
    background-color: var(--wel-pagination-current-bg);
    color: var(--wel-pagination-current-ink);
  }

  /* Current is background-only in normal rendering, so forced colors
     gives it a border (the docs/09 rule) and drops the trail underline. */
  @media (forced-colors: active) {
    .pagination :where(a[aria-current="page"]) {
      border: var(--wel-border-width-strong) solid CanvasText;
      text-decoration: none;
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .popover — transient anchored surface on the Popover API
   (docs/components/popover-menu.md, T-41). The headline: popovertarget
   makes open/close/toggle, light dismiss, Esc, one-at-a-time, top layer,
   and the invoker's expanded state fully declarative — zero JS. Core
   placement is the DESIGNED centred panel (top-layer boxes cannot tether
   without anchor positioning; the reset restores the UA margin: auto this
   relies on); Enhanced tethers to the implicit invoker anchor. Menus are
   plain lists of links/buttons — role="menu" without the full APG keyboard
   pattern is a defect, not an enhancement (spec Pattern B, post-v1). */
@layer components {
  .popover {
    --wel-popover-bg: var(--wel-color-surface-raised);
    --wel-popover-ink: var(--wel-color-ink);
    --wel-popover-radius: var(--wel-radius-surface);
    --wel-popover-padding: var(--wel-space-2);
    --wel-popover-shadow: var(--wel-shadow-3);
    --wel-popover-min-inline-size: 12rem;
    --wel-popover-max-inline-size: min(24rem, calc(100dvw - 2 * var(--wel-space-gutter)));
    --wel-popover-offset: var(--wel-space-1);

    min-inline-size: var(--wel-popover-min-inline-size);
    max-inline-size: var(--wel-popover-max-inline-size);
    padding: var(--wel-popover-padding);
    background-color: var(--wel-popover-bg);
    color: var(--wel-popover-ink);
    /* Hairline normally; THE panel edge under forced colors, where the
       shadow is stripped. */
    border: var(--wel-border-width) solid var(--wel-color-border);
    border-radius: var(--wel-popover-radius);
    box-shadow: var(--wel-popover-shadow);
  }

  /* Menu list: items pad themselves to ~24px+ targets (2.5.8). */
  .popover > :where(ul) {
    list-style: none;
    padding: 0;
    display: flex;
    flex-direction: column;
  }

  .popover :where(ul) :where(a, button) {
    display: flex;
    align-items: center;
    gap: var(--wel-space-2);
    inline-size: 100%;
    padding-block: var(--wel-space-2);
    padding-inline: var(--wel-space-2);
    min-block-size: 1.5rem;
    color: inherit;
    text-decoration: none;
    text-align: start;
    background: none;
    border: 0;
    border-radius: var(--wel-radius-sm);
    font: inherit;
  }

  /* Enhanced: tether to the invoker's implicit anchor. inset: auto is
     load-bearing — the UA popover style is inset: 0, which silently
     defeats position-area (see tooltip.css, T-45). margin becomes the
     anchor gap; outside this block it stays the UA auto centring. */
  @supports (anchor-name: --wel-a) {
    .popover {
      inset: auto;
      position-area: block-end;
      position-try-fallbacks: flip-block;
      margin: var(--wel-popover-offset);
    }
  }

  /* Enhanced: fade + 2% scale entry, fade exit, riding --wel-motion. */
  @supports (transition-behavior: allow-discrete) {
    .popover {
      opacity: 0;
      scale: 0.98;
      transition:
        opacity calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
        scale calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
        display calc(var(--wel-motion-duration-1) * var(--wel-motion)) allow-discrete,
        overlay calc(var(--wel-motion-duration-1) * var(--wel-motion)) allow-discrete;
    }

    .popover:where(:popover-open) {
      opacity: 1;
      scale: 1;
    }

    @starting-style {
      .popover:where(:popover-open) {
        opacity: 0;
        scale: 0.98;
      }
    }
  }
}

@layer variants {
  .popover[data-size="sm"] {
    --wel-popover-padding: var(--wel-space-1);
    --wel-popover-min-inline-size: 9rem;
  }

  /* data-placement is Enhanced-only by contract: without anchor
     positioning every placement is the same centred panel. */
  @supports (anchor-name: --wel-a) {
    .popover[data-placement="block-start"] {
      position-area: block-start;
    }

    .popover[data-placement="inline-start"] {
      position-area: inline-start;
      position-try-fallbacks: flip-inline;
    }

    .popover[data-placement="inline-end"] {
      position-area: inline-end;
      position-try-fallbacks: flip-inline;
    }
  }
}

@layer states {
  @media (hover: hover) {
    .popover :where(ul) :where(a, button):hover {
      background-color: var(--wel-color-accent-tint);
    }
  }

  /* Current item: accent ink AND an inline-start marker — never colour
     alone (survives forced colors as the border). */
  .popover :where(ul) :where([aria-current]) {
    color: var(--wel-color-accent);
    border-inline-start: var(--wel-border-width-strong) solid var(--wel-color-accent);
    border-start-start-radius: 0;
    border-end-start-radius: 0;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .progress / .spinner / .skeleton — loading indicators (docs/components/
   progress-spinner-skeleton.md, T-37). Progress = measurable, spinner =
   indeterminate, skeleton = known shape (aria-hidden, inside an
   [aria-busy] region — the region announces, never the placeholder).
   Every duration is token-derived through the --wel-motion multiplier. */
@layer components {
  /* ---- Progress: native element. Quick path = accent-color (correct in
     every engine); the full restyle rides engine pseudos. An unrecognised
     pseudo-element invalidates its whole rule, so each engine's block is
     a separate rule — never comma-joined. Engines rejecting the pseudos
     keep the accent-coloured native bar. */
  .progress {
    --wel-progress-track-bg: var(--wel-color-surface-sunken);
    --wel-progress-fill: var(--wel-color-accent);
    --wel-progress-block-size: var(--wel-space-2);
    --wel-progress-radius: var(--wel-radius-full);

    accent-color: var(--wel-progress-fill);
    appearance: none;
    inline-size: 100%;
    block-size: var(--wel-progress-block-size);
    border: none;
    background-color: var(--wel-progress-track-bg);
    border-radius: var(--wel-progress-radius);
  }

  .progress::-webkit-progress-bar {
    background-color: var(--wel-progress-track-bg);
    border-radius: var(--wel-progress-radius);
  }

  .progress::-webkit-progress-value {
    background-color: var(--wel-progress-fill);
    border-radius: var(--wel-progress-radius);
  }

  .progress::-moz-progress-bar {
    background-color: var(--wel-progress-fill);
    border-radius: var(--wel-progress-radius);
  }

  /* ---- Spinner: border-built arc over a visible track. At multiplier 0
     or PRM the rotation stops and the static partial arc still reads as
     "busy"; the role="status" label does the announcing. */
  .spinner {
    --wel-spinner-size: var(--wel-space-6);
    --wel-spinner-thickness: var(--wel-border-width-strong);
    --wel-spinner-ink: var(--wel-color-accent);
    --wel-spinner-track: var(--wel-color-border);

    display: inline-block;
    inline-size: var(--wel-spinner-size);
    block-size: var(--wel-spinner-size);
    border: var(--wel-spinner-thickness) solid var(--wel-spinner-track);
    border-block-start-color: var(--wel-spinner-ink);
    border-radius: var(--wel-radius-full);
    animation: wel-spinner-rotate calc(var(--wel-motion-duration-4) * 2 * var(--wel-motion)) linear infinite;
  }

  @media (prefers-reduced-motion: reduce) {
    .spinner {
      animation: none;
    }
  }

  /* ---- Skeleton: static tinted shape in Core; the shimmer exists only
     inside the motion-OK wrapper — stillness is the default. */
  .skeleton {
    --wel-skeleton-bg: var(--wel-color-surface-sunken);
    --wel-skeleton-shimmer: var(--wel-color-surface-raised);
    --wel-skeleton-radius: var(--wel-radius-sm);

    background-color: var(--wel-skeleton-bg);
    border-radius: var(--wel-skeleton-radius);
  }

  @media (prefers-reduced-motion: no-preference) {
    .skeleton {
      background-image: linear-gradient(90deg, transparent 25%, var(--wel-skeleton-shimmer) 50%, transparent 75%);
      background-size: 200% 100%;
      background-repeat: no-repeat;
      animation: wel-skeleton-shimmer calc(var(--wel-motion-duration-4) * 3 * var(--wel-motion)) var(--wel-motion-ease) infinite;
    }
  }

  /* Enhanced (docs/03): a THEME zeroing --wel-motion also halts the two
     infinite animations, not just the OS preference. */
  @container style(--wel-motion: 0) {
    .spinner,
    .skeleton {
      animation: none;
    }
  }

  /* Forced colors: progress reverts to native (fill maps to Highlight);
     spinner maps arc/track explicitly; skeletons gain a border so the
     reserved space stays visible. */
  @media (forced-colors: active) {
    .progress {
      appearance: auto;
    }

    .spinner {
      border-color: GrayText;
      border-block-start-color: CanvasText;
    }

    .skeleton {
      border: 1px solid GrayText;
    }
  }
}

@layer variants {
  .progress[data-size="sm"] { --wel-progress-block-size: var(--wel-space-1); }
  .progress[data-size="lg"] { --wel-progress-block-size: var(--wel-space-3); }

  /* sm is 1em-relative for the inline-in-button pattern. */
  .spinner[data-size="sm"] {
    --wel-spinner-size: 1em;
    --wel-spinner-thickness: 0.125em;
  }

  .spinner[data-size="lg"] {
    --wel-spinner-size: var(--wel-space-8);
  }

  .skeleton[data-variant="text"] {
    block-size: 1em;
  }

  .skeleton[data-variant="circle"] {
    --wel-skeleton-radius: var(--wel-radius-full);

    inline-size: var(--wel-space-7);
    aspect-ratio: 1;
  }
}

@layer states {
  /* Indeterminate progress: no value to fill — a subtle track pulse
     (opacity only, multiplier-gated; docs steer to the spinner). */
  .progress:where(:indeterminate) {
    animation: wel-progress-pulse calc(var(--wel-motion-duration-4) * 4 * var(--wel-motion)) var(--wel-motion-ease) infinite alternate;
  }
}

@keyframes wel-spinner-rotate {
  to {
    rotate: 1turn;
  }
}

@keyframes wel-skeleton-shimmer {
  from {
    background-position: 200% 0;
  }

  to {
    background-position: -200% 0;
  }
}

@keyframes wel-progress-pulse {
  to {
    opacity: 0.6;
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .prose — editorial typography for element-only content (docs/components/
   prose.md, T-36): rendered Markdown, CMS output, documentation. base
   makes elements presentable; this makes a document composed. The one
   component where descendant ELEMENT selectors are the API — and every
   rule is `.prose :where(...)` so the whole file sits at exactly 0-1-0:
   one unlayered class overrides anything here, and components dropped
   into an article beat prose rules by normal layer order. */
@layer components {
  .prose {
    --wel-prose-measure: var(--wel-text-measure);
    --wel-prose-gap: var(--wel-space-4);
    --wel-prose-heading-gap: calc(var(--wel-prose-gap) * 2);
    --wel-prose-ink: var(--wel-color-ink);
    --wel-prose-code-bg: var(--wel-color-surface-sunken);
    --wel-prose-quote-accent: var(--wel-color-border-strong);
    --wel-prose-rule: var(--wel-color-border);

    max-inline-size: var(--wel-prose-measure);
    color: var(--wel-prose-ink);
  }

  /* Rhythm: margins, not fixed heights (1.4.12) — the owl gives every
     following block the base gap; headings get double ABOVE only, so they
     bind to the content they introduce. */
  .prose > :where(* + *) {
    margin-block-start: var(--wel-prose-gap);
  }

  .prose > :where(h1, h2, h3, h4, h5, h6):where(:not(:first-child)) {
    margin-block-start: var(--wel-prose-heading-gap);
  }

  .prose :where(h1, h2, h3, h4, h5, h6) {
    text-wrap: balance;
  }

  /* Lists */
  .prose :where(ul, ol) {
    padding-inline-start: var(--wel-space-5);
  }

  .prose :where(li + li),
  .prose :where(li) > :where(ul, ol) {
    margin-block-start: var(--wel-space-1);
  }

  /* Blockquote */
  .prose :where(blockquote) {
    padding-inline-start: var(--wel-space-4);
    border-inline-start: var(--wel-border-width-strong) solid var(--wel-prose-quote-accent);
    color: var(--wel-color-ink-muted);
  }

  .prose :where(blockquote) > :where(* + *) {
    margin-block-start: var(--wel-prose-gap);
  }

  /* Code — token redirect; components layer wins over base, so the
     pre>code reset must be restated. */
  .prose :where(code, kbd, pre) {
    background: var(--wel-prose-code-bg);
  }

  .prose :where(pre) :where(code) {
    background: none;
  }

  /* Figures */
  .prose :where(figcaption) {
    margin-block-start: var(--wel-space-2);
    color: var(--wel-color-ink-muted);
    font-size: var(--wel-text-size--1);
  }

  /* Tables: hairline rules from the prose rule token. (A table wider than
     the measure is the content author's scroller to add — prose cannot
     wrap it, and display overrides would strip table semantics.) */
  .prose :where(table) {
    inline-size: 100%;
    border-collapse: collapse;
  }

  .prose :where(th, td) {
    padding-block: var(--wel-space-1);
    padding-inline: var(--wel-space-2);
    border-block-end: var(--wel-border-width) solid var(--wel-prose-rule);
  }

  .prose :where(hr) {
    border-block-start-color: var(--wel-prose-rule);
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .select — native <select>, styled as far as the platform honestly allows
   (docs/components/select.md, T-43). Core tier only: appearance: base-select
   is pre-Baseline (Chromium-only) and ADR-0012 bars it from shipped source;
   the spec's Enhanced tier is defined for intake the day it completes. The
   picker stays UA-rendered by policy — color-scheme on :root already keeps
   it dark in dark mode, which beats any Core styling trick. No JS, ever:
   rebuilding <select> is the anti-pattern this component exists to end. */
@layer components {
  .select {
    --wel-select-bg: var(--wel-color-surface);
    --wel-select-ink: var(--wel-color-ink);
    --wel-select-border: var(--wel-color-border-strong);
    --wel-select-radius: var(--wel-radius-control);
    --wel-select-padding-block: var(--wel-space-2);
    --wel-select-padding-inline: var(--wel-space-3);
    --wel-select-caret-color: var(--wel-color-ink-muted);
    --wel-select-caret-size: 0.4em;

    appearance: none;
    padding-block: var(--wel-select-padding-block);
    padding-inline: var(--wel-select-padding-inline);
    /* Reserve the caret's lane so long labels never run under it. */
    padding-inline-end: calc(var(--wel-select-padding-inline) + var(--wel-select-caret-size) + var(--wel-space-2));
    /* WCAG 2.5.8: every size keeps a ≥ 24px block-size target. */
    min-block-size: 1.5rem;
    min-inline-size: 8rem;
    background-color: var(--wel-select-bg);
    color: var(--wel-select-ink);
    border: var(--wel-border-width) solid var(--wel-select-border);
    border-radius: var(--wel-select-radius);
    font: inherit;
    font-size: var(--wel-text-size-0);
    line-height: var(--wel-text-leading-tight);
    /* Caret: two gradient-drawn half-squares forming a chevron. Gradients —
       unlike url() images — resolve custom properties and light-dark(), so
       the caret genuinely consumes --wel-select-caret-color in both schemes
       and under [data-theme] pinning. The ±0.5px stop softens the diagonal. */
    background-image:
      linear-gradient(135deg, var(--wel-select-caret-color) calc(50% - 0.5px), transparent calc(50% + 0.5px)),
      linear-gradient(45deg, transparent calc(50% - 0.5px), var(--wel-select-caret-color) calc(50% + 0.5px));
    background-position:
      calc(100% - var(--wel-select-padding-inline)) 50%,
      calc(100% - var(--wel-select-padding-inline) - var(--wel-select-caret-size) / 2) 50%;
    background-size: calc(var(--wel-select-caret-size) / 2) var(--wel-select-caret-size);
    background-repeat: no-repeat;
    transition: border-color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease);
  }

  /* background-position has no flow-relative syntax — a permitted physical
     exception (ADR-0009). :dir() (not [dir]) so mixed-direction subtrees
     resolve correctly; :where() keeps the budget at 0-1-0 (ADR-0010). */
  .select:where(:dir(rtl)) {
    /* The 45deg half must stay inline-left of the 135deg half or the
       chevron breaks into two corners. */
    background-position:
      calc(var(--wel-select-padding-inline) + var(--wel-select-caret-size) / 2) 50%,
      var(--wel-select-padding-inline) 50%;
    padding-inline-end: var(--wel-select-padding-inline);
    padding-inline-start: calc(var(--wel-select-padding-inline) + var(--wel-select-caret-size) + var(--wel-space-2));
  }

  /* The canonical .field wrapper owns the column; the trigger fills it. */
  :where(.field) .select {
    inline-size: 100%;
  }

  /* Forced colors: hand rendering back wholesale — appearance: auto restores
     the native arrow (the gradient caret would be stripped with backgrounds
     anyway) and the control's Field/FieldText mappings are the correct
     experience. The one place in the toolkit we revert on purpose. */
  @media (forced-colors: active) {
    .select {
      appearance: auto;
      background-image: none;
    }
  }
}

@layer variants {
  .select[data-size="sm"] {
    --wel-select-padding-block: var(--wel-space-1);
    --wel-select-padding-inline: var(--wel-space-2);

    font-size: var(--wel-text-size--1);
  }

  .select[data-size="lg"] {
    --wel-select-padding-block: var(--wel-space-3);
    --wel-select-padding-inline: var(--wel-space-4);

    font-size: var(--wel-text-size-1);
  }
}

@layer states {
  @media (hover: hover) {
    .select:hover {
      border-color: var(--wel-color-ink-muted);
    }
  }

  /* Focus ring is the single global :focus-visible rule in base. */

  .select:user-invalid {
    border-color: var(--wel-color-danger);
  }

  /* Disabled last in the layer so it beats hover at equal specificity.
     Reduced-contrast pair is the documented WCAG 1.4.3 exemption. */
  .select:disabled {
    background-color: var(--wel-color-surface-sunken);
    color: var(--wel-color-ink-faint);
    border-color: var(--wel-color-border);
    --wel-select-caret-color: var(--wel-color-ink-faint);
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .table + .table-scroller — tabular data (docs/components/table.md, T-35).
   The scroller is part of the contract: overflow-x (physical — ADR-0009
   exception until overflow-inline graduates) preserves the table's 2-D
   grid at any width. No cell-stacking "responsive" mode, by explicit spec
   rejection — overriding table display types strips its implicit ARIA.
   Sort STYLING (aria-sort) ships here; the sort behaviour module is the
   JS-enhanced add-on (ADR-0011), not this file. */
@layer components {
  .table-scroller {
    overflow-x: auto;
  }

  .table {
    --wel-table-border: var(--wel-color-border);
    --wel-table-bg: var(--wel-color-surface);
    --wel-table-stripe-bg: var(--wel-color-surface-sunken);
    --wel-table-hover-bg: var(--wel-color-accent-tint);
    --wel-table-head-ink: var(--wel-color-ink-muted);
    --wel-table-cell-padding-block: var(--wel-space-2);
    --wel-table-cell-padding-inline: var(--wel-space-3);
    --wel-table-sticky-shadow: var(--wel-shadow-1);

    inline-size: 100%;
    background-color: var(--wel-table-bg);
    border-collapse: collapse;
  }

  .table :where(th, td) {
    padding-block: var(--wel-table-cell-padding-block);
    padding-inline: var(--wel-table-cell-padding-inline);
    border-block-end: var(--wel-border-width) solid var(--wel-table-border);
  }

  .table :where(thead th) {
    color: var(--wel-table-head-ink);
  }

  .table > :where(caption) {
    padding-block: var(--wel-space-2);
  }

  /* Forced colors: stripe/hover backgrounds are stripped by the mode, so
     row separation must never depend on them — every row keeps a
     CanvasText rule, and the sticky edge affordance becomes a border. */
  @media (forced-colors: active) {
    .table :where(th, td) {
      border-block-end: var(--wel-border-width) solid CanvasText;
    }
  }
}

@layer variants {
  .table[data-variant="striped"] :where(tbody tr:nth-child(even)) {
    background-color: var(--wel-table-stripe-bg);
  }

  .table[data-variant="ruled"] :where(th, td) {
    border: var(--wel-border-width) solid var(--wel-table-border);
  }

  .table[data-size="sm"] {
    --wel-table-cell-padding-block: var(--wel-space-1);
    --wel-table-cell-padding-inline: var(--wel-space-2);

    font-size: var(--wel-text-size--1);
  }

  .table[data-size="lg"] {
    --wel-table-cell-padding-block: var(--wel-space-3);
    --wel-table-cell-padding-inline: var(--wel-space-4);
  }

  /* Sticky cells must be opaque — content scrolls under them. */
  .table[data-sticky="header"] :where(thead th),
  .table[data-sticky="both"] :where(thead th) {
    position: sticky;
    inset-block-start: 0;
    background-color: var(--wel-table-bg);
    box-shadow: var(--wel-table-sticky-shadow);
  }

  .table[data-sticky="first-column"] :where(tr > :first-child),
  .table[data-sticky="both"] :where(tr > :first-child) {
    position: sticky;
    inset-inline-start: 0;
    background-color: var(--wel-table-bg);
    box-shadow: var(--wel-table-sticky-shadow);
  }

  /* Enhanced (docs/03 additive-only): the sticky-edge shadow appears only
     once the scroller has actually scrolled — a true affordance. Fallback
     keeps the shadow permanently visible. Scroll-driven, not timed, so no
     --wel-motion multiplier applies (nothing moves). */
  @supports (animation-timeline: scroll()) {
    .table-scroller {
      scroll-timeline: --wel-table-scroll inline;
    }

    .table[data-sticky="first-column"] :where(tr > :first-child),
    .table[data-sticky="both"] :where(tr > :first-child) {
      animation: wel-table-edge linear both;
      animation-timeline: --wel-table-scroll;
      animation-range: 0 3rem;
    }
  }

  @media (forced-colors: active) {
    .table[data-sticky] :where(th, td) {
      box-shadow: none;
    }

    .table[data-sticky="first-column"] :where(tr > :first-child),
    .table[data-sticky="both"] :where(tr > :first-child) {
      border-inline-end: var(--wel-border-width) solid CanvasText;
    }
  }
}

@layer states {
  @media (hover: hover) {
    .table[data-hover] :where(tbody tr):hover {
      background-color: var(--wel-table-hover-bg);
    }
  }

  /* Current row: tint plus a start bar — never background-only. */
  .table :where(tr[aria-current="true"]) {
    background-color: var(--wel-color-accent-tint);
    border-inline-start: var(--wel-border-width-strong) solid var(--wel-color-accent);
  }

  /* The sort upgrader wraps header content in a real <button> (or
     enhances a server sort link in place): both render as the header
     text, not control chrome — aria-sort and the glyph carry the state,
     and the global focus-visible ring stays intact. */
  .table :where(th[data-sort]) :where(button, a) {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: inherit;
    text-align: inherit;
    text-decoration: none;
    cursor: pointer;
  }

  /* Sorted column: header ink strengthens; direction glyph is generated
     content in currentColor (survives forced colors). aria-sort itself is
     the announcement — the glyph is declared empty for AT where the
     alternative-text syntax is supported. */
  .table :where(th[aria-sort]) {
    color: var(--wel-color-ink);
  }

  .table :where(th[aria-sort="ascending"])::after {
    content: " ↑";
  }

  .table :where(th[aria-sort="descending"])::after {
    content: " ↓";
  }

  @supports (content: "a" / "b") {
    .table :where(th[aria-sort="ascending"])::after {
      content: " ↑" / "";
    }

    .table :where(th[aria-sort="descending"])::after {
      content: " ↓" / "";
    }
  }

  .table[aria-busy="true"] :where(tbody) {
    opacity: 0.5;
    pointer-events: none;
  }
}

@keyframes wel-table-edge {
  from {
    box-shadow: none;
  }

  to {
    box-shadow: var(--wel-table-sticky-shadow);
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* wel-tabs — APG tabs, JS-enhanced (docs/components/tabs.md, T-47).
   Everything here keys to the roles/attributes wel-tabs.js GENERATES
   (role=tab, aria-selected) — no Enhanced CSS is required for a fully
   styled widget. Pre-upgrade/no-JS the authored heading+section stack
   renders with deliberate rhythm via :not(:defined) and reserves no
   tablist space, so upgrade costs one reflow. The selected indicator
   is a real ::after box so the module's view transition has geometry
   to morph; the group name arrives via --wel-tabs-vt only while a
   change is in flight (see wel-tabs.js), and the morph duration rides
   --wel-motion, so reduced motion zeroes it. */
@layer components {
  wel-tabs {
    --wel-tab-ink: var(--wel-color-ink-muted);
    --wel-tab-ink-selected: var(--wel-color-ink);
    --wel-tab-indicator: var(--wel-color-accent);
    --wel-tabs-border: var(--wel-color-border);
    --wel-tab-padding-block: var(--wel-space-2);
    --wel-tab-padding-inline: var(--wel-space-3);
    --wel-tabs-gap: var(--wel-space-2);

    display: block;
  }

  /* Pre-upgrade / no-JS: stacked, headed sections with .stack rhythm. */
  wel-tabs:not(:defined) > * + * {
    margin-block-start: var(--wel-space-4);
  }

  wel-tabs [role="tablist"] {
    display: flex;
    gap: var(--wel-tabs-gap);
    border-block-end: var(--wel-border-width) solid var(--wel-tabs-border);
    /* Overflowing tabs scroll rather than wrap — wrapped tab rows
       misread as two tablists. Physical axis per ADR-0009 exception. */
    overflow-x: auto;
    scroll-snap-type: x proximity;
  }

  wel-tabs [role="tab"] {
    position: relative;
    /* The authored headings arrive with heading margins and type. */
    margin: 0;
    padding-block: var(--wel-tab-padding-block);
    padding-inline: var(--wel-tab-padding-inline);
    color: var(--wel-tab-ink);
    font-size: var(--wel-text-size-0);
    font-weight: 500;
    line-height: var(--wel-text-leading-tight);
    white-space: nowrap;
    scroll-snap-align: start;
    cursor: pointer;
    transition: color calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease);
  }

  /* Underline indicator (default variant), sitting on the tablist rule. */
  wel-tabs [role="tab"]::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    block-size: 3px;
  }

  wel-tabs [role="tab"][aria-selected="true"] {
    color: var(--wel-tab-ink-selected);
  }

  wel-tabs [role="tab"][aria-selected="true"]::after {
    background-color: var(--wel-tab-indicator);
    view-transition-name: var(--wel-tabs-vt, none);
  }

  wel-tabs [role="tabpanel"] {
    padding-block-start: var(--wel-space-4);
  }

  /* The morphing indicator group; 0ms under reduced motion. */
  ::view-transition-group(wel-tab-indicator) {
    animation-duration: calc(var(--wel-motion-duration-2) * var(--wel-motion));
  }
}

@layer variants {
  /* Pills: the fill is the indicator, so the tab itself carries the
     view-transition name and the underline box is dropped. */
  wel-tabs[data-variant="pills"] [role="tablist"] {
    border-block-end: 0;
  }

  wel-tabs[data-variant="pills"] [role="tab"] {
    border-radius: var(--wel-radius-control);
  }

  wel-tabs[data-variant="pills"] [role="tab"]::after {
    content: none;
  }

  wel-tabs[data-variant="pills"] [role="tab"][aria-selected="true"] {
    background-color: var(--wel-tab-indicator);
    color: var(--wel-color-accent-contrast);
    view-transition-name: var(--wel-tabs-vt, none);
  }
}

@layer states {
  @media (hover: hover) {
    wel-tabs [role="tab"]:not([aria-selected="true"]):hover {
      color: var(--wel-tab-ink-selected);
    }
  }

  /* Selection must survive without backgrounds (09): borders, not fills. */
  @media (forced-colors: active) {
    wel-tabs [role="tab"][aria-selected="true"] {
      border-block-end: 3px solid Highlight;
    }

    wel-tabs[data-variant="pills"] [role="tab"][aria-selected="true"] {
      border: var(--wel-border-width-strong) solid Highlight;
      /* Explicit system pair: the UA already forces both, but axe reads
         the authored accent-contrast ink through the forcing and fails
         the pair it never renders. */
      background-color: Canvas;
      color: CanvasText;
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* wel-toast / wel-toast-region — transient notifications
   (docs/components/toast.md, T-49). Pre-upgrade the region is an
   in-flow stack and every toast reads as a static tone-styled alert —
   that IS the no-JS baseline, so the overlay positioning is gated on
   :defined (CSS applies without JS; an unconditioned fixed overlay
   would break the fallback). Tone is never colour alone: the strong
   inline-start border pairs with the authored icon/text. Entry
   slide/fade rides @starting-style; exit is the [data-closing] fade
   wel-toast-region.js waits for; the stack reflow morph is a view
   transition (per-toast names set by the module; the duration rule
   below targets their shared view-transition-class). */
@layer components {
  wel-toast-region {
    --wel-toast-gap: var(--wel-space-2);

    display: flex;
    flex-direction: column;
    gap: var(--wel-toast-gap);
  }

  /* Upgraded: viewport-anchored page chrome (06 scaffolding exception). */
  wel-toast-region:defined {
    position: fixed;
    inset-block-end: var(--wel-space-gutter);
    inset-inline-end: var(--wel-space-gutter);
    z-index: 10;
    max-inline-size: calc(100dvw - 2 * var(--wel-space-gutter));
    align-items: flex-end;
  }

  wel-toast {
    --wel-toast-bg: var(--wel-color-surface-raised);
    --wel-toast-ink: var(--wel-color-ink);
    --wel-toast-border: var(--wel-color-border);
    --wel-toast-accent: var(--wel-color-ink-muted);
    --wel-toast-radius: var(--wel-radius-surface);
    --wel-toast-shadow: var(--wel-shadow-3);
    --wel-toast-max-inline-size: 24rem;

    display: flex;
    align-items: center;
    gap: var(--wel-space-2);
    inline-size: min(100%, var(--wel-toast-max-inline-size));
    padding-block: var(--wel-space-2);
    padding-inline: var(--wel-space-3);
    background-color: var(--wel-toast-bg);
    color: var(--wel-toast-ink);
    border: var(--wel-border-width) solid var(--wel-toast-border);
    border-inline-start: var(--wel-border-width-strong) solid var(--wel-toast-accent);
    border-radius: var(--wel-toast-radius);
    box-shadow: var(--wel-toast-shadow);
  }

  /* Generated dismiss button hugs the inline end. */
  wel-toast > :where(button:last-child) {
    margin-inline-start: auto;
  }

  /* Enhanced: slide/fade entry when a toast becomes visible, fade exit
     while the module waits. Durations ride --wel-motion (0 under PRM,
     and the module then removes immediately). */
  @supports (transition-behavior: allow-discrete) {
    wel-toast-region:defined wel-toast {
      transition:
        opacity calc(var(--wel-motion-duration-2) * var(--wel-motion)) var(--wel-motion-ease),
        translate calc(var(--wel-motion-duration-2) * var(--wel-motion)) var(--wel-motion-ease);
    }

    @starting-style {
      wel-toast-region:defined wel-toast {
        opacity: 0;
        translate: 0 0.5rem;
      }
    }

    wel-toast-region:defined wel-toast[data-closing] {
      opacity: 0;
    }
  }

  /* The stack-reflow morph groups (module-driven view transition). */
  ::view-transition-group(.wel-toast) {
    animation-duration: calc(var(--wel-motion-duration-2) * var(--wel-motion));
  }
}

@layer variants {
  wel-toast[data-tone="info"]    { --wel-toast-bg: var(--wel-color-info-tint);    --wel-toast-accent: var(--wel-color-info); }
  wel-toast[data-tone="success"] { --wel-toast-bg: var(--wel-color-success-tint); --wel-toast-accent: var(--wel-color-success); }
  wel-toast[data-tone="warning"] { --wel-toast-bg: var(--wel-color-warning-tint); --wel-toast-accent: var(--wel-color-warning); }
  wel-toast[data-tone="danger"]  { --wel-toast-bg: var(--wel-color-danger-tint);  --wel-toast-accent: var(--wel-color-danger); }

  /* Placement is an upgraded-only axis (pre-upgrade the region is in flow). */
  wel-toast-region:defined[data-placement="start"] {
    inset-inline-end: auto;
    inset-inline-start: var(--wel-space-gutter);
    align-items: flex-start;
  }

  /* inset-inline + auto margins centre without physical translate (RTL-safe). */
  wel-toast-region:defined[data-placement="center"] {
    inset-inline: 0;
    inline-size: fit-content;
    margin-inline: auto;
    align-items: center;
  }
}

@layer states {
  /* Shadow disappears in forced colors; the border is the edge. */
  @media (forced-colors: active) {
    wel-toast {
      border-color: CanvasText;
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* .tooltip — plain-text supplementary label on the Popover API
   (docs/components/tooltip.md, T-45). Core is a toggletip: popovertarget
   toggles it, Esc/light-dismiss close it, and aria-describedby exposes the
   text to AT with zero interaction — the popover UA default centres it
   (top-layer boxes cannot tether without anchor positioning). Enhanced
   tethers it to the trigger's implicit invoker anchor. Hover behaviour is
   wel-tooltip.js (Phase 5); popover="hint" is runtime-detected there, never
   markup (its invalid-value fallback is `manual`, which would silently drop
   Esc + light dismiss). */
@layer components {
  .tooltip {
    --wel-tooltip-bg: var(--wel-color-ink);
    --wel-tooltip-ink: var(--wel-color-surface);
    --wel-tooltip-radius: var(--wel-radius-control);
    --wel-tooltip-padding-block: var(--wel-space-1);
    --wel-tooltip-padding-inline: var(--wel-space-2);
    --wel-tooltip-max-inline-size: 18rem;
    --wel-tooltip-offset: var(--wel-space-1);
    /* Interaction timing for wel-tooltip (read via getComputedStyle), not
       motion — deliberately not multiplied by --wel-motion: a zeroed delay
       would make tooltips MORE eager under reduced motion. */
    --wel-tooltip-show-delay: 600ms;

    padding-block: var(--wel-tooltip-padding-block);
    padding-inline: var(--wel-tooltip-padding-inline);
    max-inline-size: var(--wel-tooltip-max-inline-size);
    background-color: var(--wel-tooltip-bg);
    color: var(--wel-tooltip-ink);
    border: 0;
    border-radius: var(--wel-tooltip-radius);
    font-size: var(--wel-text-size--1);
    line-height: var(--wel-text-leading-tight);
  }

  /* Enhanced: tether to the trigger via the implicit invoker anchor.
     margin is the trigger gap here; outside this block the UA's auto
     margins keep the Core fallback centred, so margin must not leak out. */
  @supports (anchor-name: --wel-a) {
    .tooltip {
      /* The UA popover style is inset: 0 — position-area only steers the
         box once the insets are auto again. */
      inset: auto;
      position-area: block-start;
      position-try-fallbacks: flip-block;
      margin: var(--wel-tooltip-offset);
    }
  }

  /* Enhanced: opacity-only fade (non-vestibular, 09) riding --wel-motion;
     display/overlay transition keeps the exit fade in the top layer. */
  @supports (transition-behavior: allow-discrete) {
    .tooltip {
      opacity: 0;
      transition:
        opacity calc(var(--wel-motion-duration-1) * var(--wel-motion)) var(--wel-motion-ease),
        display calc(var(--wel-motion-duration-1) * var(--wel-motion)) allow-discrete,
        overlay calc(var(--wel-motion-duration-1) * var(--wel-motion)) allow-discrete;
    }

    .tooltip:where(:popover-open) {
      opacity: 1;
    }

    @starting-style {
      .tooltip:where(:popover-open) {
        opacity: 0;
      }
    }
  }

  /* Forced colors: the inverted pair collapses to Canvas/CanvasText — a
     border keeps the bubble's edge. */
  @media (forced-colors: active) {
    .tooltip {
      border: var(--wel-border-width) solid CanvasText;
    }
  }
}

@layer variants {
  /* data-placement is Enhanced-only by contract: without anchor
     positioning every placement is the same centred fallback. */
  @supports (anchor-name: --wel-a) {
    .tooltip[data-placement="block-end"] {
      position-area: block-end;
    }

    .tooltip[data-placement="inline-start"] {
      position-area: inline-start;
      position-try-fallbacks: flip-inline;
    }

    .tooltip[data-placement="inline-end"] {
      position-area: inline-end;
      position-try-fallbacks: flip-inline;
    }
  }
}

@layer reset, tokens, base, layout, components, variants, states, utilities, overrides;

/* utilities layer — single-purpose classes; the only layer permitted
   !important (ADR-0010). */
@layer utilities {
  /* Visually hidden, available to assistive tech (docs/09; consumed by
     count badges, skip links, icon-only labels). The standard clip-path
     recipe; !important so no component rule can accidentally reveal or
     resize it. */
  .visually-hidden {
    position: absolute !important;
    inline-size: 1px !important;
    block-size: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip-path: inset(50%) !important;
    white-space: nowrap !important;
    border: 0 !important;
  }

  /* Duotone image treatment (T-103) — wrapper class around img/video.
     The media is desaturated, then two full-bleed overlays gradient-map it:
     lighten raises blacks to the shadow colour, darken pulls whites to the
     highlight colour (result: min(max(pixel, shadow), highlight)).
     Endpoints derive from the cascaded --wel-color-accent at fixed
     lightnesses, so one accent override retints the whole effect (docs/05
     derivation pattern); override the two endpoint tokens directly for a
     custom mapping. The media is forced to fill the wrapper's inline size —
     the overlays cover the wrapper, and any uncovered backdrop would blend
     to a solid colour slab. */
  .duotone {
    --wel-duotone-shadow: oklch(from var(--wel-color-accent) 0.28 c h);
    --wel-duotone-highlight: oklch(from var(--wel-color-accent) 0.95 calc(c / 3) h);

    position: relative;
    isolation: isolate; /* blending stops here, never the page backdrop */
  }

  .duotone :is(img, video) {
    display: block;
    inline-size: 100%;
    filter: grayscale(1);
  }

  /* Dark-mode dimming (T-102) — apply to the media element; the token
     resolves to the identity filter in light scheme and a gentle
     brightness/contrast pull-down in dark (tokens/color.css). */
  .dim {
    filter: var(--wel-img-dim);
  }

  /* Composing with .duotone: its child selector outranks .dim, so the
     combination is stated explicitly — dim the grayscale base, then the
     overlays remap it (dark mode duotones read less glary too). */
  .duotone :is(img.dim, video.dim) {
    filter: var(--wel-img-dim) grayscale(1);
  }

  .duotone::before,
  .duotone::after {
    content: "";
    position: absolute;
    inset: 0;
    /* the media's filter makes it a stacking context painted at the z-0
       level in tree order, i.e. above a z-auto ::before — lift both
       overlays over it (tree order keeps ::after above ::before) */
    z-index: 1;
    pointer-events: none;
    border-radius: inherit;
  }

  .duotone::before {
    background-color: var(--wel-duotone-shadow);
    mix-blend-mode: lighten;
  }

  /* ::after paints above ::before, so darken applies to the lightened
     composite — the order the gradient map needs. */
  .duotone::after {
    background-color: var(--wel-duotone-highlight);
    mix-blend-mode: darken;
  }

  /* Forced colors: backgrounds are forced, which would turn the overlays
     into opaque system-colour slabs over the media — drop the effect and
     show the original image. */
  @media (forced-colors: active) {
    .duotone::before,
    .duotone::after {
      display: none;
    }

    .duotone :is(img, video) {
      filter: none;
    }
  }

  /* Edge-fade mask (T-106) — the element melts into whatever is behind it
     (hero bleeds, sidebar media). Applied directly to the media (or any
     element); no wrapper. The fade band is --wel-edge-fade deep. Default
     fades all four edges: two gradient layers intersected. data-edges
     narrows the fade to one axis or edge; "radial" is an elliptical
     vignette. Engines without mask-image show the unfaded image — a clean
     static fallback (image-FX doctrine, D-11).
     Gradient directions are physical (CSS has no logical gradient
     keywords): correct for horizontal-tb; the asymmetric inline variants
     get :dir(rtl) flips below, vertical writing modes are out of scope. */
  .edge-fade {
    --wel-edge-fade: var(--wel-space-8);

    mask-image:
      linear-gradient(to bottom, transparent, black var(--wel-edge-fade), black calc(100% - var(--wel-edge-fade)), transparent),
      linear-gradient(to right, transparent, black var(--wel-edge-fade), black calc(100% - var(--wel-edge-fade)), transparent);
    mask-composite: intersect;
  }

  .edge-fade[data-edges="block"] {
    mask-image: linear-gradient(to bottom, transparent, black var(--wel-edge-fade), black calc(100% - var(--wel-edge-fade)), transparent);
  }

  .edge-fade[data-edges="inline"] {
    mask-image: linear-gradient(to right, transparent, black var(--wel-edge-fade), black calc(100% - var(--wel-edge-fade)), transparent);
  }

  .edge-fade[data-edges="block-start"] {
    mask-image: linear-gradient(to bottom, transparent, black var(--wel-edge-fade));
  }

  .edge-fade[data-edges="block-end"] {
    mask-image: linear-gradient(to bottom, black calc(100% - var(--wel-edge-fade)), transparent);
  }

  .edge-fade[data-edges="inline-start"] {
    mask-image: linear-gradient(to right, transparent, black var(--wel-edge-fade));
  }

  .edge-fade[data-edges="inline-end"] {
    mask-image: linear-gradient(to right, black calc(100% - var(--wel-edge-fade)), transparent);
  }

  .edge-fade[data-edges="inline-start"]:dir(rtl) {
    mask-image: linear-gradient(to left, transparent, black var(--wel-edge-fade));
  }

  .edge-fade[data-edges="inline-end"]:dir(rtl) {
    mask-image: linear-gradient(to left, black calc(100% - var(--wel-edge-fade)), transparent);
  }

  .edge-fade[data-edges="radial"] {
    mask-image: radial-gradient(farthest-side, black calc(100% - var(--wel-edge-fade)), transparent);
  }

  /* Scroll-driven reveal (T-108) — fade/scale-in driven by the element's
     own viewport entry (animation-timeline: view()), zero JS. The hidden
     start state exists ONLY inside this gate: engines without scroll-
     driven animations (and reduced-motion users, per the docs/09 matrix —
     scroll-driven effects gate off entirely) simply see the element,
     never a stuck opacity-0 (image-FX doctrine, D-11). Timeline-driven,
     so no duration for the --wel-motion multiplier to ride — the media
     gate is the reduced-motion mechanism here (carousel precedent).
     Tune with --wel-reveal-scale (start scale) and --wel-reveal-distance
     (start block-axis offset; positive rises into place). */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .reveal {
        --wel-reveal-scale: 0.96;
        --wel-reveal-distance: 0px;

        animation: wel-reveal var(--wel-motion-ease-out) both;
        animation-timeline: view();
        animation-range: entry;
      }
    }
  }

  /* Frosted caption bar (T-104) — wrapper (canonically a <figure>) whose
     <figcaption> pins to the media's block-end edge as a translucent
     surface bar, backdrop-blurred where supported. This is the family's
     sanctioned text-on-imagery pattern (image-fx spec): the bar is the
     ink/surface pairing over a surface-colour scrim, so text never sits on
     raw photography. overflow: clip means a border-radius on the wrapper
     crops media and caption together — round the wrapper, not the media. */
  .frosted-caption {
    --wel-frosted-blur: 16px;
    --wel-frosted-saturate: 1.8;
    --wel-frosted-bg: color-mix(in oklch, var(--wel-color-surface) 68%, transparent);

    position: relative;
    overflow: clip;
  }

  .frosted-caption :is(img, video) {
    display: block;
    inline-size: 100%; /* the caption spans the wrapper — media must fill it */
  }

  /* No-backdrop-filter engines skip the @supports block below and keep this
     near-solid scrim: text stays readable over any image (never-a-broken-
     image doctrine) at the cost of the glass look. Forced colors needs no
     rule — background-color is forced to an opaque system colour there. */
  .frosted-caption > figcaption {
    position: absolute;
    inset-block-end: 0;
    inset-inline: 0;
    /* above a composed .duotone's z-index:1 blend overlays (probe D-12) —
       at z-auto the caption would paint under them and get gradient-mapped */
    z-index: 2;
    padding: var(--wel-space-2) var(--wel-space-4);
    font-size: var(--wel-text-size--1);
    color: var(--wel-color-ink);
    background-color: color-mix(in oklch, var(--wel-color-surface) 92%, transparent);
  }

  @supports (backdrop-filter: blur(1px)) {
    .frosted-caption > figcaption {
      background-color: var(--wel-frosted-bg);
      backdrop-filter: blur(var(--wel-frosted-blur)) saturate(var(--wel-frosted-saturate));
    }
  }

  /* docs/09 preference matrix: translucent surface treatments fall back to
     an opaque equivalent under prefers-reduced-transparency. */
  @media (prefers-reduced-transparency: reduce) {
    .frosted-caption > figcaption {
      background-color: var(--wel-color-surface);
      backdrop-filter: none;
    }
  }

  /* Adaptive crop (T-112) — one class, container-driven art direction: in a
     narrow layout container the media crops tight and square (presence over
     panorama in a cramped column); at 30rem+ (the card spec's established
     switch width) it stretches to a full-bleed cinematic band. Queries the
     nearest layout primitive like every container-aware component; with no
     layout ancestor neither branch matches and the media keeps its natural
     aspect — plain image, never broken. Retune per place by overriding the
     two ratio tokens; the query widths are fixed (container queries cannot
     read custom properties in their conditions). */
  .adaptive-crop {
    --wel-crop-narrow: 1;
    --wel-crop-wide: 21 / 9;

    display: block;
    inline-size: 100%;
    object-fit: cover;
  }

  @container layout (inline-size < 30rem) {
    .adaptive-crop {
      aspect-ratio: var(--wel-crop-narrow);
    }
  }

  @container layout (inline-size >= 30rem) {
    .adaptive-crop {
      aspect-ratio: var(--wel-crop-wide);
    }
  }

  /* Grayscale-to-color hover reveal (T-105) — media rests desaturated and
     saturates on hover, or on keyboard focus when wrapped in a link/button.
     The whole effect lives inside @media (hover: hover): touch/stylus users
     have no way to trigger the reveal, so they get the plain colour image
     (never-a-broken-image doctrine) rather than one stuck grayscale.
     The transition rides the --wel-motion multiplier like every duration in
     the codebase — reduced-motion users get an instant swap, not a fade.
     filter is the family's contended property (image-fx spec): the .dim
     combo is stated explicitly below; .duotone media is already grayscale
     by construction, so .color-reveal inside it is meaningless — forbidden
     nesting, spec-documented in T-122. */
  @media (hover: hover) {
    .color-reveal {
      --wel-color-reveal-rest: 1;

      filter: grayscale(var(--wel-color-reveal-rest));
      transition: filter calc(var(--wel-motion-duration-3) * var(--wel-motion)) var(--wel-motion-ease);
    }

    .color-reveal:is(:hover, :focus-visible),
    :is(a, button, summary):is(:hover, :focus-visible) .color-reveal {
      filter: grayscale(0);
    }

    /* .dim composes: same list shape in rest and hover states, so the
       grayscale argument interpolates while the dim prefix holds. */
    .color-reveal.dim {
      filter: var(--wel-img-dim) grayscale(var(--wel-color-reveal-rest));
    }

    .color-reveal.dim:is(:hover, :focus-visible),
    :is(a, button, summary):is(:hover, :focus-visible) .color-reveal.dim {
      filter: var(--wel-img-dim) grayscale(0);
    }
  }

  /* Organic frames (T-107) — blob (default) / arch / scallop crops via
     clip-path: shape(), applied to the media element directly (like
     .edge-fade; clipping a wrapper would crop captions/overlays too).
     Every coordinate is a percentage, so the crop scales with any box and
     aspect ratio (responsive-safe). Engines without shape() keep the
     ungated border-radius approximations below — blob and arch have honest
     radius analogues, scallop falls back to a plain rounded crop; never a
     broken image. Inside the gate the fallback radius is zeroed so the two
     clips don't intersect at the corners. */
  .organic-frame {
    border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; /* blob analogue */
  }

  .organic-frame[data-shape="arch"] {
    border-radius: 50% 50% 0 0 / 45% 45% 0 0;
  }

  .organic-frame[data-shape="scallop"] {
    border-radius: var(--wel-radius-lg);
  }

  @supports (clip-path: shape(from 0% 0%, line to 100% 100%)) {
    .organic-frame,
    .organic-frame[data-shape="arch"],
    .organic-frame[data-shape="scallop"] {
      border-radius: 0;
    }

    /* five-anchor cubic loop; lobes stay off the corners */
    .organic-frame {
      clip-path: shape(
        from 55% 3%,
        curve to 96% 32% with 78% 10% / 95% 12%,
        curve to 87% 82% with 98% 52% / 99% 68%,
        curve to 35% 97% with 72% 99% / 55% 104%,
        curve to 4% 56% with 16% 92% / -2% 78%,
        curve to 55% 3% with 8% 28% / 32% -4%,
        close
      );
    }

    /* elliptical arch: quadrant cubics (k = 0.552) over straight jambs */
    .organic-frame[data-shape="arch"] {
      clip-path: shape(
        from 0% 100%,
        line to 0% 40%,
        curve to 50% 0% with 0% 17.9% / 22.4% 0%,
        curve to 100% 40% with 77.6% 0% / 100% 17.9%,
        line to 100% 100%,
        close
      );
    }

    /* a 6%-inset loop with six quadratic bumps per edge (peaks reach ~1%
       from the box edge); corner points stay sharp */
    .organic-frame[data-shape="scallop"] {
      clip-path: shape(
        from 6% 6%,
        curve to 20.67% 6% with 13.33% -4%,
        curve to 35.33% 6% with 28% -4%,
        curve to 50% 6% with 42.67% -4%,
        curve to 64.67% 6% with 57.33% -4%,
        curve to 79.33% 6% with 72% -4%,
        curve to 94% 6% with 86.67% -4%,
        curve to 94% 20.67% with 104% 13.33%,
        curve to 94% 35.33% with 104% 28%,
        curve to 94% 50% with 104% 42.67%,
        curve to 94% 64.67% with 104% 57.33%,
        curve to 94% 79.33% with 104% 72%,
        curve to 94% 94% with 104% 86.67%,
        curve to 79.33% 94% with 86.67% 104%,
        curve to 64.67% 94% with 72% 104%,
        curve to 50% 94% with 57.33% 104%,
        curve to 35.33% 94% with 42.67% 104%,
        curve to 20.67% 94% with 28% 104%,
        curve to 6% 94% with 13.33% 104%,
        curve to 6% 79.33% with -4% 86.67%,
        curve to 6% 64.67% with -4% 72%,
        curve to 6% 50% with -4% 57.33%,
        curve to 6% 35.33% with -4% 42.67%,
        curve to 6% 20.67% with -4% 28%,
        curve to 6% 6% with -4% 13.33%,
        close
      );
    }
  }

  /* Ambient glow (T-115) — wrapper class (like .duotone): a halo behind the
     media. Default is a two-layer accent-derived drop-shadow — it follows
     the composite's silhouette, so an .organic-frame crop inside glows in
     its clipped shape; retint the whole halo with one --wel-color-accent
     override, or --wel-glow-color directly. The filter lives on the WRAPPER,
     so the family's contended media filter (.dim/.color-reveal/.duotone) is
     untouched. Engines without relative colour invalidate the shadow colour
     and drop the halo — plain image, never broken. */
  .glow {
    --wel-glow-color: oklch(from var(--wel-color-accent) l c h / 55%);
    --wel-glow-size: 1.5rem;

    position: relative;
    isolation: isolate; /* keeps the ambient pseudo's z-index: -1 inside */
    filter:
      drop-shadow(0 0 calc(var(--wel-glow-size) / 2) var(--wel-glow-color))
      drop-shadow(0 0 var(--wel-glow-size) var(--wel-glow-color));
  }

  .glow :is(img, video) {
    display: block;
    inline-size: 100%;
  }

  /* Ambilight variant — the halo is the image's own colours: give the
     wrapper the same source via a custom property
     (<figure class="glow" data-glow="ambient" style="--wel-glow-image:
     url(hero.jpg)">). The blurred cover-fit pseudo sits behind the media;
     without --wel-glow-image it paints nothing and the element behaves
     like a plain wrapper (never broken). */
  .glow[data-glow="ambient"] {
    filter: none;
  }

  .glow[data-glow="ambient"]::before {
    content: "";
    position: absolute;
    inset: calc(-1 * var(--wel-glow-size) / 2);
    z-index: -1;
    background-image: var(--wel-glow-image, none);
    background-size: cover;
    background-position: center;
    filter: blur(var(--wel-glow-size)) saturate(1.5);
    pointer-events: none;
  }

  /* Forced colors: a decorative coloured halo has no forced-colors story —
     drop it and show the plain media. */
  @media (forced-colors: active) {
    .glow {
      filter: none;
    }

    .glow[data-glow="ambient"]::before {
      display: none;
    }
  }

  /* Squircle corners (T-116) — superellipse crop on media: the continuous-
     curvature "iOS icon" corner. corner-shape reshapes how the border-radius
     corners are drawn, so the fallback is built in: engines without it drop
     the unknown property and render the same radius as plain round corners —
     no gate needed, never a broken image. The generous default radius is the
     point (squircles read at sizes where round corners look bulbous); tune
     with --wel-squircle-radius. */
  .squircle {
    --wel-squircle-radius: 25%;

    border-radius: var(--wel-squircle-radius);
    corner-shape: squircle;
  }

  /* Parallax depth (T-110) — wrapper class (like .duotone): the media
     drifts along the block axis as the wrapper crosses the viewport,
     driven by the media's own view() timeline — zero JS, the .reveal
     scroll-timeline conventions. The wrapper clips; the media is scaled
     up for headroom so the drift never exposes a gap. Keep
     --wel-parallax-scale >= 1 + 2 * --wel-parallax-depth (as a fraction:
     the default 1.2 covers the default 8% drift with margin).
     Everything kinetic lives inside the gate: engines without
     scroll-driven animations — and reduced-motion users, per the docs/09
     matrix (timeline-driven, no duration for the --wel-motion multiplier
     to zero) — get an unscaled, motionless image; the ungated
     overflow: clip is then a no-op. Never a broken image. */
  .parallax {
    position: relative;
    overflow: clip;
  }

  .parallax :is(img, video) {
    display: block;
    inline-size: 100%;
  }

  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .parallax {
        --wel-parallax-depth: 8%;
        --wel-parallax-scale: 1.2;
      }

      .parallax :is(img, video) {
        scale: var(--wel-parallax-scale);
        animation: wel-parallax linear both;
        animation-timeline: view();
        animation-range: cover;
      }
    }
  }

  /* Ken Burns (T-111) — wrapper class: the media slowly zooms and pans
     while the card is hovered (or holds keyboard focus — :has(
     :focus-visible), the card spec's pattern), then eases back quickly.
     The drift is a seconds-scale linear transition-duration on the hover
     state; the rest state keeps a short duration-4 return so letting go
     never means a 12s crawl home. Both ride the --wel-motion multiplier.
     The whole effect is motion-safe wrapped: a slow pan/zoom has no
     non-vestibular reduced equivalent (an instant zoom would *crop*, not
     calm), so under prefers-reduced-motion — and on non-hover devices —
     the media simply never moves: plain image, never broken. Pan is a
     translate percentage pair of the media's own box; the zoom's
     (--wel-kenburns-zoom - 1) / 2 overflow per side is the headroom the
     pan must stay inside (defaults: 7.5% headroom, 2%/1% pan). Writes
     the media scale/translate — the same properties .parallax animates —
     so the two never share a wrapper. */
  @media (hover: hover) {
    @media (prefers-reduced-motion: no-preference) {
      .ken-burns {
        --wel-kenburns-zoom: 1.15;
        --wel-kenburns-pan: -2% 1%;
        --wel-kenburns-duration: 12s;

        overflow: clip;
      }

      .ken-burns :is(img, video) {
        display: block;
        inline-size: 100%;
        transition:
          scale calc(var(--wel-motion-duration-4) * var(--wel-motion)) var(--wel-motion-ease-out),
          translate calc(var(--wel-motion-duration-4) * var(--wel-motion)) var(--wel-motion-ease-out);
      }

      .ken-burns:is(:hover, :has(:focus-visible)) :is(img, video) {
        scale: var(--wel-kenburns-zoom);
        translate: var(--wel-kenburns-pan);
        transition-duration: calc(var(--wel-kenburns-duration) * var(--wel-motion));
        transition-timing-function: linear;
      }
    }
  }

  /* View-box crop (T-113) — CSS art direction on the media element: crop
     into the *source image* with object-view-box, no extra image files.
     Bare class is a punch-in zoom (inset all round); data-crop keeps one
     half — names are PHYSICAL on purpose, unlike .edge-fade's logical
     axes: they point at where the subject sits in the photograph, which
     does not move with text direction. Arbitrary crops go through the
     token (--wel-view-box: inset(t r b l) / xywh(…)). object-fit: cover
     pairs with it so a view box whose ratio differs from the element's
     covers rather than distorts. Fallback is built in (AC-260): engines
     without object-view-box drop the unknown property at parse time and
     the ungated cover is a no-op at the image's natural ratio — the
     full, uncropped image, never a broken one. */
  .view-crop {
    --wel-view-box: inset(15%);

    object-view-box: var(--wel-view-box);
    object-fit: cover;
  }

  .view-crop[data-crop="top"] {
    --wel-view-box: inset(0% 0% 50% 0%);
  }

  .view-crop[data-crop="bottom"] {
    --wel-view-box: inset(50% 0% 0% 0%);
  }

  .view-crop[data-crop="left"] {
    --wel-view-box: inset(0% 50% 0% 0%);
  }

  .view-crop[data-crop="right"] {
    --wel-view-box: inset(0% 0% 0% 50%);
  }

  /* 3D hover tilt (T-114) — on the media element (or a whole card): a
     subtle fixed perspective rotation plus a whisper of lift on hover,
     or keyboard focus via the .color-reveal trigger pair. Pure CSS means
     the tilt is a single resting pose, not pointer-tracked — the depth
     cue comes from the settle, so the transition gets the spring ease.
     Writes `transform` (perspective must precede the rotates in one
     list), which composes after — but shouldn't be combined with — the
     .ken-burns/.parallax scale/translate on the same node. Hover-gated
     like .color-reveal (touch users get the plain image), and the whole
     effect nests inside prefers-reduced-motion: no-preference per the
     docs/09 matrix — only opacity/filter swaps may survive reduction,
     so reduced-motion users get an image that simply never tilts. */
  @media (hover: hover) {
    @media (prefers-reduced-motion: no-preference) {
      .tilt {
        --wel-tilt-x: 4deg;
        --wel-tilt-y: -4deg;
        --wel-tilt-perspective: 900px;
        --wel-tilt-lift: 1.02;

        transition: transform calc(var(--wel-motion-duration-3) * var(--wel-motion)) var(--wel-motion-ease-spring);
      }

      .tilt:is(:hover, :focus-visible),
      :is(a, button, summary):is(:hover, :focus-visible) .tilt {
        transform:
          perspective(var(--wel-tilt-perspective))
          rotateX(var(--wel-tilt-x))
          rotateY(var(--wel-tilt-y))
          scale(var(--wel-tilt-lift));
      }
    }
  }

  /* Textured reveal (T-117) — the image prints through an SVG data-URI
     mask pattern: a staggered halftone dot grid (default) or fractal
     grain. Two mask layers, default add-composited: the pattern reads
     full-strength, and a uniform --wel-texture-base floor keeps the
     rest of the image ghosted underneath so the treatment is a texture,
     not an obliteration (0% = hard print). Halftone tile scales with
     --wel-texture-size; grain tiles at its natural 300px (stitched
     seamless). Same mask infrastructure as .edge-fade — and the same
     contended mask-image, so never combine the two on one node: put
     .edge-fade on a wrapper when both are wanted (a wrapper's mask
     applies to everything inside). Engines without mask-image show the
     plain unmasked image; forced colors keeps it (geometry, not
     colour), matching .edge-fade. */
  .textured {
    --wel-texture-size: 0.5rem;
    --wel-texture-base: 35%;

    mask-image:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Ccircle cx='4' cy='4' r='2.2'/%3E%3Ccircle cx='0' cy='0' r='2.2'/%3E%3Ccircle cx='8' cy='0' r='2.2'/%3E%3Ccircle cx='0' cy='8' r='2.2'/%3E%3Ccircle cx='8' cy='8' r='2.2'/%3E%3C/svg%3E"),
      linear-gradient(rgb(0 0 0 / var(--wel-texture-base)), rgb(0 0 0 / var(--wel-texture-base)));
    mask-size: var(--wel-texture-size) var(--wel-texture-size), auto;
  }

  .textured[data-texture="grain"] {
    mask-image:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='gamma' exponent='0.6'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23g)'/%3E%3C/svg%3E"),
      linear-gradient(rgb(0 0 0 / var(--wel-texture-base)), rgb(0 0 0 / var(--wel-texture-base)));
    mask-size: auto, auto;
  }
}

@keyframes wel-reveal {
  from {
    opacity: 0;
    translate: 0 var(--wel-reveal-distance, 0px);
    scale: var(--wel-reveal-scale, 0.96);
  }
}

/* linear: progress is scroll-linked, easing belongs to the scroll itself */
@keyframes wel-parallax {
  from {
    translate: 0 var(--wel-parallax-depth, 8%);
  }

  to {
    translate: 0 calc(-1 * var(--wel-parallax-depth, 8%));
  }
}
