/*!
 * LoamUI — @loamui/core
 * The complete, static stylesheet. Import once at your app root:
 *   import "@loamui/core/styles.css";
 * Nothing runs at runtime — no CSS-in-JS.
 */

@layer loamui.tokens, loamui.elements, loamui.components;

/**
 * LoamUI — design tokens (the `loamui.tokens` layer).
 * Components reference tokens, never raw values, so theming is token
 * remapping at any scope. Bands: inputs → neutrals → derived → scales.
 * Every colour is prefixed `--loam-color-`; there are no raw colour
 * primitives on this surface — the inputs ARE the palette, and everything
 * else is derived from them.
 */

/* Registered channels (must live outside a layer). The radius channel is
   set inline by several components; without inherits: false it would
   cascade into everything nested inside them. The contextualism channel
   inherits by design — nearest ancestor that sets it wins. */
@property --_radius {
  inherits: false;
  syntax: "*";
}

@property --loam-context {
  inherits: true;
  syntax: "*";
}

@property --loam-button-color {
  inherits: true;
  syntax: "*";
}

@layer loamui.tokens {
  :root {
    /* ── 1. Inputs — the decisions a theme makes ───────────────────── */

    /* Two font families, both system by default — a theme swaps either.
       Body carries copy; display carries headings (designs typically pair
       a body and a display face). */
    --loam-font: system-ui, sans-serif;
    --loam-font-display: var(--loam-font);
    --loam-font-mono: ui-monospace, "SF Mono", "JetBrains Mono", menlo, consolas, monospace;

    /* Primary is the neutral action colour — near-black in light, near-white
       in dark. That is the white-label default: a theme remaps this one line
       to brand. Success owns green; accent is the small chromatic flourish
       (violet by default, for indicators and highlights), kept distinct in hue
       from the status colours so the two roles never read as the same. */
    --loam-color-primary: light-dark(oklch(25% 0.006 60deg), oklch(92% 0.004 60deg));
    --loam-color-accent: light-dark(oklch(48% 0.25 305deg), oklch(70% 0.17 305deg));

    /* Status hues. Each derives the same family as primary (soft, strong,
       ring), so a context region can remap the family wholesale. */
    --loam-color-success: light-dark(oklch(56% 0.13 150deg), oklch(68% 0.13 150deg));
    --loam-color-danger: light-dark(oklch(58% 0.2 25deg), oklch(66% 0.19 25deg));
    --loam-color-warning: light-dark(oklch(72% 0.16 75deg), oklch(79% 0.15 80deg));
    --loam-color-info: light-dark(oklch(58% 0.16 245deg), oklch(68% 0.15 245deg));

    /* Link: accessible blue — the most legible default for `a` tags. A
       project can override with the primary colour where the brand calls
       for it. */
    --loam-color-link: light-dark(oklch(50% 0.12 250deg), oklch(70% 0.12 250deg));

    /* Highlight: bright yellow for `<mark>` and `::selection`, matching the
       web platform's own default. */
    --loam-color-highlight: light-dark(oklch(92% 0.15 100deg), oklch(86% 0.16 100deg));

    /* ── 2. Neutrals ───────────────────────────────────────────────── */

    /* A whisper of warmth (hue 60, a loam/soil tone) keeps the greys from
       reading cold, and — unlike a green-adjacent neutral — sits far from every
       status hue, so a soft neutral surface never reads as success. */
    --loam-color-bg: light-dark(oklch(100% 0 0deg), oklch(17% 0.008 60deg));
    --loam-color-bg-subtle: light-dark(oklch(98% 0.004 60deg), oklch(21% 0.008 60deg));
    --loam-color-surface: light-dark(oklch(100% 0 0deg), oklch(23% 0.008 60deg));

    /* Foreground — text, but also icons, illustrations and chart fills;
       strong and dim/muted variants. Backgrounds are never dimmed. */
    --loam-color-fg: light-dark(oklch(24% 0.02 60deg), oklch(96% 0.006 60deg));
    --loam-color-fg-strong: light-dark(oklch(15% 0.02 60deg), oklch(99% 0 0deg));
    --loam-color-fg-muted: light-dark(oklch(52% 0.02 60deg), oklch(72% 0.015 60deg));
    --loam-color-fg-dim: light-dark(oklch(55% 0.015 60deg), oklch(66% 0.012 60deg));

    /* Line — borders, and separators too. */
    --loam-color-line: light-dark(oklch(91% 0.006 60deg), oklch(32% 0.01 60deg));
    --loam-color-line-strong: light-dark(oklch(62% 0.012 60deg), oklch(60% 0.014 60deg));

    /* The one foreground that reads on every solid status fill: near-white
       in light (where the fills darken), near-black in dark (where the
       fills stay raw) — the only pairing all statuses hold AA with. */
    --loam-color-on-strong: light-dark(oklch(99% 0 0deg), oklch(17% 0.01 60deg));

    /* ── 3. Derived — formulas, never hand-tuned per status ────────── */

    /* Soft tints: pinned lightness and chroma, the colour's own hue —
       relative colour keeps every soft on one formula, so a rebrand or a
       new status derives its tint for free. */
    --loam-color-primary-soft: light-dark(
      oklch(from var(--loam-color-primary) 96% 0.02 h),
      oklch(from var(--loam-color-primary) 28% 0.03 h)
    );
    --loam-color-success-soft: light-dark(
      oklch(from var(--loam-color-success) 96% 0.035 h),
      oklch(from var(--loam-color-success) 31% 0.075 h)
    );
    --loam-color-danger-soft: light-dark(
      oklch(from var(--loam-color-danger) 96% 0.035 h),
      oklch(from var(--loam-color-danger) 31% 0.075 h)
    );
    --loam-color-warning-soft: light-dark(
      oklch(from var(--loam-color-warning) 96% 0.035 h),
      oklch(from var(--loam-color-warning) 31% 0.075 h)
    );
    --loam-color-info-soft: light-dark(
      oklch(from var(--loam-color-info) 96% 0.035 h),
      oklch(from var(--loam-color-info) 31% 0.075 h)
    );

    /* Solid-fill variants: AA for --loam-color-on-strong. In light the colour
       darkens 22%; in dark the raw colour already carries dark text.
       Audited pairs — see scripts/contrast-audit.mjs. */
    --loam-color-primary-strong: light-dark(
      color-mix(in oklab, var(--loam-color-primary), oklch(0% 0 0deg) 22%),
      var(--loam-color-primary)
    );
    --loam-color-success-strong: light-dark(
      color-mix(in oklab, var(--loam-color-success), oklch(0% 0 0deg) 22%),
      var(--loam-color-success)
    );
    --loam-color-danger-strong: light-dark(
      color-mix(in oklab, var(--loam-color-danger), oklch(0% 0 0deg) 22%),
      var(--loam-color-danger)
    );
    --loam-color-warning-strong: light-dark(
      color-mix(in oklab, var(--loam-color-warning), oklch(0% 0 0deg) 22%),
      var(--loam-color-warning)
    );
    --loam-color-info-strong: light-dark(
      color-mix(in oklab, var(--loam-color-info), oklch(0% 0 0deg) 22%),
      var(--loam-color-info)
    );

    /* Hover lift for surfaces: derived, so a re-themed surface keeps its
       own hover for free. */
    --loam-color-surface-hover: light-dark(
      oklch(from var(--loam-color-surface) 97% c h),
      oklch(from var(--loam-color-surface) 27% c h)
    );

    /* Active focus-ring colour, remapped contextually (e.g. fields with a
       detected error switch it to the danger fill). There is no separate ring
       family: a ring is the status's own solid fill, which is already audited
       to 3:1 against the background. Pairs with the fixed --loam-ring-width
       below. */
    --loam-color-ring: var(--loam-color-primary-strong);

    /* ── 4. Scales & fixed constants ───────────────────────────────── */

    /* Fluid type scale — cqi falls back to the small viewport where no
       container is declared. Regenerate, don't hand-edit:
       https://utopia.fyi/type/calculator/?c=320,16,1.125,1240,18,1.25,4,2 */
    --loam-text-xs: clamp(0.72rem, 0.8145rem - 0.122cqi, 0.7901rem);
    --loam-text-sm: clamp(0.8889rem, 0.885rem + 0.0193cqi, 0.9rem);
    --loam-text-md: clamp(1rem, 0.9565rem + 0.2174cqi, 1.125rem);
    --loam-text-lg: clamp(1.125rem, 1.0272rem + 0.4891cqi, 1.4063rem);
    --loam-text-xl: clamp(1.2656rem, 1.0944rem + 0.856cqi, 1.7578rem);
    --loam-text-2xl: clamp(1.4238rem, 1.1548rem + 1.3451cqi, 2.1973rem);
    --loam-text-3xl: clamp(1.6018rem, 1.2036rem + 1.9909cqi, 2.7466rem);

    /* Fluid spacing scale — minimums are the fixed values, so nothing
       shrinks below them. Every step grows 12.5% between a 20rem container
       and a 77.5rem one, and grows against the CONTAINER (cqi), never the
       viewport: a gap in a sidebar answers the sidebar. Regenerate, don't
       hand-edit:
       https://utopia.fyi/space/calculator/?c=320,16,1.125,1240,18,1.25&s=0.125|0.25|0.5|0.75,1.25|1.5|2|3|4|6 */
    --loam-space-4xs: clamp(0.125rem, 0.1196rem + 0.0272cqi, 0.1406rem);
    --loam-space-3xs: clamp(0.25rem, 0.2391rem + 0.0543cqi, 0.2812rem);
    --loam-space-2xs: clamp(0.5rem, 0.4783rem + 0.1087cqi, 0.5625rem);
    --loam-space-xs: clamp(0.75rem, 0.7174rem + 0.163cqi, 0.8438rem);
    --loam-space-s: clamp(1rem, 0.9565rem + 0.2174cqi, 1.125rem);
    --loam-space-m: clamp(1.25rem, 1.1957rem + 0.2717cqi, 1.4062rem);
    --loam-space-l: clamp(1.5rem, 1.4348rem + 0.3261cqi, 1.6875rem);
    --loam-space-xl: clamp(2rem, 1.913rem + 0.4348cqi, 2.25rem);
    --loam-space-2xl: clamp(3rem, 2.8696rem + 0.6522cqi, 3.375rem);
    --loam-space-3xl: clamp(4rem, 3.8261rem + 0.8696cqi, 4.5rem);
    --loam-space-4xl: clamp(6rem, 5.7391rem + 1.3043cqi, 6.75rem);

    /* Fixed spacing — the deliberate opposite of the scale above, for the
       things that must not breathe: a hairline, the inset of a focus ring,
       the gap between an icon and its own label. Reach for the fluid scale
       first; use these only where growth would be wrong. */
    --loam-space-fixed-1: 1px;
    --loam-space-fixed-2: 2px;
    --loam-space-fixed-4: 4px;
    --loam-space-fixed-8: 8px;
    --loam-space-fixed-12: 12px;
    --loam-space-fixed-16: 16px;
    --loam-space-fixed-24: 24px;
    --loam-space-fixed-32: 32px;

    /* Reading measure — the comfortable line length for body copy (~66
       characters). Cap a prose column with `max-inline-size:
       var(--loam-measure)` so long text stays readable. */
    --loam-measure: 66ch;

    /* Radius — deliberately fixed: corner rounding shouldn't breathe with
       the viewport. */
    --loam-radius-sm: 0.375rem;
    --loam-radius-md: 0.5rem;
    --loam-radius-lg: 0.75rem;
    --loam-radius-xl: 1rem;
    --loam-radius-full: 9999px;

    /* Elevation — a real drop in light; in dark the drop goes transparent
       and an inset top highlight carries the bevel, so the light source
       stays consistent (a shadow below a surface makes no sense on a dark
       canvas). */
    --loam-shadow-sm:
      inset 0 1px 0 0 light-dark(transparent, oklch(100% 0 0deg / 6%)),
      0 1px 2px light-dark(oklch(20% 0.02 60deg / 8%), transparent);
    --loam-shadow-raised:
      inset 0 1px 0 0 light-dark(oklch(100% 0 0deg / 60%), oklch(100% 0 0deg / 10%)),
      0 1px 2px light-dark(oklch(20% 0.02 60deg / 10%), transparent),
      0 2px 4px light-dark(oklch(20% 0.02 60deg / 7%), transparent),
      0 4px 8px light-dark(oklch(20% 0.02 60deg / 4%), transparent);
    --loam-shadow-md:
      inset 0 1px 0 0 light-dark(transparent, oklch(100% 0 0deg / 6%)),
      0 4px 6px -1px light-dark(oklch(20% 0.02 60deg / 10%), transparent),
      0 2px 4px -2px light-dark(oklch(20% 0.02 60deg / 8%), transparent);
    --loam-shadow-lg:
      inset 0 1px 0 0 light-dark(transparent, oklch(100% 0 0deg / 8%)),
      0 12px 20px -6px light-dark(oklch(20% 0.02 60deg / 14%), transparent),
      0 4px 8px -4px light-dark(oklch(20% 0.02 60deg / 10%), transparent);

    /* Motion — sm: micro feedback; md: default transitions; lg: overlay
       enter/exit and large movement; elastic: sparingly. */
    --loam-duration-sm: 100ms;
    --loam-duration-md: 175ms;
    --loam-duration-lg: 300ms;
    --loam-ease: cubic-bezier(0.4, 0, 0.2, 1);
    --loam-ease-elastic: cubic-bezier(0.34, 1.56, 0.64, 1);

    /* Depth — one ladder for everything that floats, so no component picks
       its own number. popup: menus, popovers, tooltips; overlay: dialogs and
       drawers; toast: notifications above dialogs; skip-link: above all. */
    --loam-z-popup: 40;
    --loam-z-overlay: 50;
    --loam-z-toast: 100;
    --loam-z-skip-link: 110;

    /* Fixed interaction constants: chosen once, not derived. Ring width pairs
       with the derived --loam-color-ring; disabled opacity may be remapped by a
       component locally. */
    --loam-ring-width: 3px;
    --loam-disabled-opacity: 0.55;

    color-scheme: light dark;

    /* No control-sizing tokens: inputs and buttons align by a shared derived
       anatomy (padding-block --loam-space-xs + --loam-text-md × 1.5 + 1px
       borders). Do not add height tokens. */
  }

  /* Manual theme override at any scope — the attribute just sets
     color-scheme, so light-dark() re-resolves for that subtree. */
  [data-theme="light"] {
    color-scheme: light;
  }

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

  /*
   * Contexts — `--loam-context: success|warning|info|danger` on any region
   * re-answers the semantic tokens via container style queries. Colour
   * tokens ONLY — never spacing, sizing, or layout.
   *
   * The remap reaches EVERY element inside the region, not only LoamUI's
   * own: a region declares what it means, so a bare control's accent, and a
   * consumer's own rule reading --loam-color-primary, answer it too. That is
   * what makes contextualism a property of all three primitives rather than
   * a feature of the components.
   */
  @container (style(--loam-context: danger)) {
    :where(*) {
      --loam-color-primary: var(--loam-color-danger);
      --loam-color-accent: var(--loam-color-danger);
      --loam-color-primary-soft: var(--loam-color-danger-soft);

      /* --loam-color-ring and --loam-color-primary-strong bake their var()s AT
         :root — custom properties resolve var() where declared, so both
         must be re-answered directly here or focus rings and solid fills
         would stay brand-coloured. */
      --loam-color-ring: var(--loam-color-danger-strong);
      --loam-color-primary-strong: var(--loam-color-danger-strong);
    }
  }

  @container (style(--loam-context: success)) {
    :where(*) {
      --loam-color-primary: var(--loam-color-success);
      --loam-color-accent: var(--loam-color-success);
      --loam-color-primary-soft: var(--loam-color-success-soft);
      --loam-color-ring: var(--loam-color-success-strong);
      --loam-color-primary-strong: var(--loam-color-success-strong);
    }
  }

  @container (style(--loam-context: warning)) {
    :where(*) {
      --loam-color-primary: var(--loam-color-warning);
      --loam-color-accent: var(--loam-color-warning);
      --loam-color-primary-soft: var(--loam-color-warning-soft);
      --loam-color-ring: var(--loam-color-warning-strong);
      --loam-color-primary-strong: var(--loam-color-warning-strong);
    }
  }

  @container (style(--loam-context: info)) {
    :where(*) {
      --loam-color-primary: var(--loam-color-info);
      --loam-color-accent: var(--loam-color-info);
      --loam-color-primary-soft: var(--loam-color-info-soft);
      --loam-color-ring: var(--loam-color-info-strong);
      --loam-color-primary-strong: var(--loam-color-info-strong);
    }
  }
}

/**
 * LoamUI — element styles (the `loamui.elements` layer).
 *
 * One of the library's three primitives: enhanced default styles for the
 * native elements themselves, page-wide. We modernise the browser's
 * sensible defaults to improve UX and accessibility — building on them,
 * not resetting them. Importing the stylesheet opts the app in; the
 * cascade keeps every rule overridable — anything the app writes outside a
 * layer wins, and the components layer wins for a component's own parts, so
 * selectors here are plain type selectors, not `:where()` armour. (The one
 * `:where()` below is a deliberate exception: it holds the additive
 * line-height default at zero specificity so a control's own leading wins.)
 *
 * Margins are additive: blocks carry only a block-end margin, and extra
 * space before a heading comes from an adjacent-sibling rule — nothing
 * needs "unsetting" at the top of a container.
 */

@layer loamui.elements {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  /* Computed leading, declared broadly on purpose: line-height inherits as a
     COMPUTED value, so a body-level calc would freeze at the body's font size.
     Per-element, every font size derives its own leading, and 2ex tracks the
     font's actual x-height. Form controls and sub/sup set their own leading
     directly, so they are excluded here rather than overridden, keeping the
     rule additive. */
  :where(:not(button, input, select, textarea, sub, sup)) {
    line-height: calc(0.5rem + 2ex);
  }

  html {
    scrollbar-gutter: stable;

    @media (prefers-reduced-motion: no-preference) {
      scroll-behavior: smooth;
    }
  }

  /* Body carries the reading text. OpenType and hyphenation defaults come
     from the platform's own typographic wisdom: old-style proportional
     figures in copy, common ligatures, hanging punctuation, and hyphenated
     justification where the language allows. */
  body {
    background-color: var(--loam-color-bg);
    color: var(--loam-color-fg);
    font-family: var(--loam-font);
    font-kerning: normal;
    font-optical-sizing: auto;
    font-size: var(--loam-text-md);
    font-variant-ligatures: common-ligatures contextual;
    font-variant-numeric: oldstyle-nums proportional-nums;
    hanging-punctuation: first allow-end last;
    hyphenate-limit-chars: 7 4 3;
    hyphens: auto;
    margin: 0;
    text-size-adjust: none;
    text-wrap: pretty;
  }

  /* ── Typography ─────────────────────────────────────────────────── */

  /* Headings use the display family and tighten slightly with size — the
     tightening is in `em`, so it scales with the font automatically (larger
     headings sit tighter). Lining figures suit display type. */
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    color: var(--loam-color-fg-strong);
    font-family: var(--loam-font-display);
    font-variant-numeric: lining-nums proportional-nums;
    letter-spacing: -0.02em;
    margin-block: 0 var(--loam-space-2xs);
    text-wrap: balance;
  }

  h1 {
    font-size: var(--loam-text-3xl);
    font-weight: 700;
  }

  h2 {
    font-size: var(--loam-text-2xl);
    font-weight: 700;
  }

  h3 {
    font-size: var(--loam-text-xl);
    font-weight: 700;
  }

  h4 {
    font-size: var(--loam-text-lg);
    font-weight: 600;
  }

  h5 {
    font-size: var(--loam-text-md);
    font-weight: 600;
  }

  h6 {
    font-size: var(--loam-text-sm);
    font-weight: 600;
  }

  /* Breathing room before a heading that follows content — additive, so
     a heading that opens its container needs no reset. */
  :is(p, ul, ol, dl, pre, blockquote, figure, table) + :is(h1, h2, h3, h4, h5, h6) {
    margin-block-start: var(--loam-space-l);
  }

  p {
    margin-block: 0 var(--loam-space-xs);
    overflow-wrap: break-word;
  }

  ul,
  ol {
    margin-block: 0 var(--loam-space-xs);
    padding-inline-start: var(--loam-space-l);
  }

  li {
    margin-block-end: var(--loam-space-3xs);
  }

  li::marker {
    color: var(--loam-color-fg-muted);
  }

  blockquote {
    border-inline-start: 3px solid var(--loam-color-line-strong);
    color: var(--loam-color-fg-muted);
    margin-block: 0 var(--loam-space-xs);
    margin-inline: 0;
    padding-inline-start: var(--loam-space-s);
  }

  hr {
    border: none;
    border-block-start: 1px solid var(--loam-color-line);
    margin-block: var(--loam-space-s);
  }

  small {
    font-size: var(--loam-text-sm);
  }

  figcaption {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);
    margin-block-start: var(--loam-space-3xs);
  }

  mark {
    background: var(--loam-color-highlight);
    border-radius: var(--loam-radius-sm);
    color: oklch(20% 0 0deg);
    padding-inline: 0.15em;
  }

  abbr[title] {
    cursor: help;
    text-decoration: underline dotted;
    text-underline-offset: 0.2em;
  }

  /* Keep super/subscripts from disturbing line rhythm. */
  sub,
  sup {
    font-size: 0.75em;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
  }

  sub {
    inset-block-start: 0.25em;
  }

  sup {
    inset-block-start: -0.5em;
  }

  table {
    border-collapse: collapse;
  }

  th {
    text-align: start;
  }

  /* Data reads as data: tabular figures so numbers line up in columns. Only
     digits are affected, so text cells are untouched. */
  td {
    font-variant-numeric: tabular-nums;
  }

  /* An amount or a date sizes nothing (it takes the type around it) and
     fixes only its figures: lining, so it sits with display type, and
     tabular, so a column stays straight. Price and Time render these
     elements and add nothing to this rule. */
  data,
  time {
    font-variant-numeric: lining-nums tabular-nums;

    /* A date keeps its own order inside right-to-left text. */
    unicode-bidi: isolate;
  }

  /* ── Links ──────────────────────────────────────────────────────── */

  /* Links default to the accessible blue, with the classic visited (toward
     purple) and active (toward red) hue shifts derived by relative colour —
     the most legible, most familiar link affordance. A project can override
     --loam-color-link with the brand where it wants to. */
  a {
    &:any-link {
      cursor: pointer;
      -webkit-tap-highlight-color: transparent;
      text-decoration-color: oklch(from currentcolor l c h / 60%);
      text-decoration-skip-ink: auto;
      text-decoration-thickness: max(0.08em, 1px);
      text-underline-offset: 0.15em;

      @media (prefers-reduced-motion: no-preference) {
        transition: text-decoration-color var(--loam-duration-md) var(--loam-ease);
      }
    }

    &:link {
      color: var(--loam-color-link);
    }

    &:visited {
      color: oklch(from var(--loam-color-link) l c calc(h + 35));
    }

    &:active {
      color: oklch(from var(--loam-color-link) l c calc(h + 100));
    }

    @media (hover: hover) {
      &:hover {
        text-decoration: none;
      }
    }
  }

  /* ── Code ───────────────────────────────────────────────────────── */

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

  :is(code, samp):not(pre *) {
    background: var(--loam-color-bg-subtle);
    border-radius: var(--loam-radius-sm);
    padding: 0.1em 0.35em;
  }

  /* A key reads as a key cap, not as code: an edge on a raised surface,
     a step smaller so a sequence of keys sits inside the line. */
  kbd {
    font-family: var(--loam-font-mono);
    font-size: 0.85em;

    &:not(pre *) {
      background: var(--loam-color-surface);
      border: 1px solid var(--loam-color-line-strong);
      border-radius: var(--loam-radius-sm);
      padding: 0.1em 0.4em;
    }

    /* The surface is gone here; the edge is what says "key", so it takes
       the control colour rather than the text one. */
    @media (forced-colors: active) {
      &:not(pre *) {
        border-color: ButtonText;
      }
    }
  }

  pre {
    background: var(--loam-color-bg-subtle);
    border-radius: var(--loam-radius-md);
    margin-block: 0 var(--loam-space-xs);

    /* overflow (shorthand), not the logical overflow-inline: the latter is not
       yet Baseline, so code blocks would not scroll in some browsers. */
    overflow: auto;
    padding: var(--loam-space-xs);
  }

  /* ── Media ──────────────────────────────────────────────────────── */

  img,
  svg,
  video,
  canvas {
    block-size: auto;
    max-inline-size: 100%;
  }

  /* ── Forms & interaction ────────────────────────────────────────── */

  button,
  input,
  select,
  textarea,
  optgroup {
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
  }

  input,
  select,
  textarea,
  progress {
    accent-color: var(--loam-color-primary-strong);
    caret-color: var(--loam-color-primary);
  }

  button,
  input,
  select,
  textarea,
  [role="button"] {
    -webkit-tap-highlight-color: transparent;
  }

  /* Native buttons wear the Button component's own base look: the same
     derivation recipe on the neutral text channel, restated rather than
     derived (a component file cannot be imported here). The pair is policed
     by scripts/contrast-audit.mjs, whose tintWeights/labelWeights read both
     files and fail when the weights drift. Real actions should still use
     <Button/>, which adds context adaptation. */
  button,
  input:is([type="button"], [type="reset"], [type="submit"]),
  ::file-selector-button {
    background: light-dark(
      color-mix(in oklab, var(--loam-color-fg), var(--loam-color-bg) 90%),
      color-mix(in oklab, var(--loam-color-fg), var(--loam-color-bg) 75%)
    );
    border: 1px solid color-mix(in oklab, var(--loam-color-fg), var(--loam-color-bg) 80%);
    border-radius: var(--loam-radius-md);
    box-shadow: var(--loam-shadow-raised);
    color: light-dark(
      color-mix(in oklab, var(--loam-color-fg) 70%, oklch(0% 0 0deg)),
      color-mix(in oklab, var(--loam-color-fg) 55%, oklch(100% 0 0deg))
    );
    cursor: pointer;
    font-size: var(--loam-text-md);
    font-weight: 600;
    line-height: 1.5;
    padding-block: var(--loam-space-xs);
    padding-inline: var(--loam-space-s);
    text-align: center;
    user-select: none;
    white-space: normal;

    &:active:not(:disabled) {
      box-shadow: var(--loam-shadow-sm);
    }

    @media (prefers-reduced-motion: no-preference) {
      transition: background var(--loam-duration-sm) var(--loam-ease);
    }

    @media (hover: hover) {
      &:hover:not(:disabled) {
        background: light-dark(
          color-mix(in oklab, var(--loam-color-fg), var(--loam-color-bg) 85%),
          color-mix(in oklab, var(--loam-color-fg), var(--loam-color-bg) 70%)
        );
      }
    }
  }

  /* Textual fields wear the Input component's field look. */
  input:not(
    [type="button"],
    [type="checkbox"],
    [type="color"],
    [type="file"],
    [type="hidden"],
    [type="image"],
    [type="radio"],
    [type="range"],
    [type="reset"],
    [type="submit"]
  ),
  select,
  textarea {
    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line-strong);
    border-radius: var(--loam-radius-md);
    font-size: var(--loam-text-md);
    line-height: 1.5;
    padding-block: var(--loam-space-xs);
    padding-inline: var(--loam-space-xs);
  }

  ::file-selector-button {
    margin-inline-end: var(--loam-space-xs);
  }

  fieldset {
    border: 1px solid var(--loam-color-line);
    border-radius: var(--loam-radius-md);
    padding: var(--loam-space-s);
  }

  legend {
    font-weight: 500;
    padding-inline: var(--loam-space-3xs);
  }

  summary {
    cursor: pointer;
    font-weight: 500;
  }

  /* Focus ring: outline offset from the element so the true surface shows
     through the gap — on any background, the ring reads as separate. The
     outline survives forced-colors, follows radius, and isn't clipped by
     overflow. */
  :focus-visible {
    outline: var(--loam-ring-width) solid var(--loam-color-ring);
    outline-offset: 2px;
  }

  :is(:disabled, [data-disabled], [aria-disabled="true"]),
  :is(:disabled, [data-disabled], [aria-disabled="true"]) * {
    cursor: not-allowed;
  }

  /* ── Page behaviours ────────────────────────────────────────────── */

  ::selection {
    background: var(--loam-color-highlight);
    color: oklch(20% 0 0deg);
  }

  /* Anchored targets never hide beneath sticky chrome. */
  [id] {
    scroll-margin-block-start: var(--loam-space-l);
  }

  /* ── Print ──────────────────────────────────────────────────────── */

  @media print {
    :is(code, kbd, samp):not(pre *),
    pre,
    mark {
      background: none;
      border: 1px solid var(--loam-color-line);
    }

    button,
    input:is([type="button"], [type="reset"], [type="submit"]),
    ::file-selector-button {
      box-shadow: none;
    }

    a[href^="http"]::after {
      content: " (" attr(href) ")";
      font-size: var(--loam-text-xs);
    }
  }
}

/* Alert */
@layer loamui.components {
/*
 * Everything here derives from a single --_accent channel (the same
 * derivation scheme as Button's --_color).
 * There is no color prop; the surrounding --loam-context region picks the
 * accent. The message stays
 * --loam-text-based so long copy remains readable on the tint.
 */
@scope (.loam-Alert) to ([class*="loam-"]) {
  :scope {
    --_accent: var(--loam-color-fg);

    align-items: flex-start;

    /* The dark tint mixes less background in — the dark page background is
       far from the channel, so an 88% mix would read near-black (same
       compensation as Button's 90%/75%). */
    background: light-dark(
      color-mix(in oklab, var(--_accent), var(--loam-color-bg) 88%),
      color-mix(in oklab, var(--_accent), var(--loam-color-bg) 70%)
    );

    /* Tints mix in oklab (cartesian), not oklch: hue interpolation against
       the near-achromatic page background rotates the channel's hue (the
       declared hue of an achromatic colour still interpolates); cartesian
       mixing only desaturates, so the channel's hue survives. */
    border: 1px solid color-mix(in oklab, var(--_accent), var(--loam-color-bg) 55%);
    border-radius: var(--loam-radius-md);
    display: block flex;
    font-size: var(--loam-text-sm);
    gap: var(--loam-space-xs);
    padding-block: var(--loam-space-xs);
    padding-inline: var(--loam-space-s);
  }

  span.icon {
    align-items: center;
    block-size: 1.25em;
    color: var(--_accent);
    display: inline flex;
    flex: none;
    font-size: var(--loam-text-md);
    inline-size: 1.25em;
    justify-content: center;
    margin-block-start: 0.05em;
  }

  /* The body takes the slack, so a Close (a Button behind the donut,
     never restyled here) sits at the inline end. */
  div.body {
    display: block flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: var(--loam-space-3xs);
    min-inline-size: 0;
  }

  .title {
    /* The title is TEXT in the channel's hue, not a swatch: mixed toward
       black (light) / white (dark) so it keeps AA contrast on the tint even
       for light channels like warning. Only the decorative icon carries the
       raw channel — the title must read. */
    color: light-dark(
      color-mix(in oklab, var(--_accent) 70%, oklch(0% 0 0deg)),
      color-mix(in oklab, var(--_accent) 55%, oklch(100% 0 0deg))
    );
    font-size: var(--loam-text-sm);
    font-weight: 600;
    line-height: 1.4;
    margin: 0;
  }

  /* The description carries full-strength text; next to a title it recedes
     to muted so the accent-coloured heading leads. */
  div.description {
    color: var(--loam-color-fg);

    :scope:has(> div.body > .title) > div.body > & {
      color: var(--loam-color-fg-muted);
    }
  }

  /* Context: within a status region — the channel takes the status colour.
     (Even though the region also remaps --loam-color-primary, the alert keys its
     own channel because its rest state is neutral text, not primary.) */
  @container (style(--loam-context: primary)) {
    :scope {
      --_accent: var(--loam-color-primary);
    }
  }

  @container (style(--loam-context: success)) {
    :scope {
      --_accent: var(--loam-color-success);
    }
  }

  @container (style(--loam-context: warning)) {
    :scope {
      --_accent: var(--loam-color-warning);
    }
  }

  @container (style(--loam-context: info)) {
    :scope {
      --_accent: var(--loam-color-info);
    }
  }

  @container (style(--loam-context: danger)) {
    :scope {
      --_accent: var(--loam-color-danger);
    }
  }
}
}

/* Avatar */
@layer loamui.components {
@scope (.loam-Avatar) to ([class*="loam-"]) {
  :scope {
    background: var(--loam-color-primary-strong);
    block-size: var(--loam-avatar-size, 2.5rem);
    border-radius: var(--_radius, var(--loam-radius-full));
    color: var(--loam-color-on-strong);
    display: inline grid;
    flex: none;
    font-size: var(--loam-text-sm);
    font-weight: 600;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    inline-size: var(--loam-avatar-size, 2.5rem);
    line-height: 1;
    overflow: clip;
    user-select: none;
  }

  img.image {
    block-size: 100%;
    display: block flow;
    grid-area: 1 / 1;
    inline-size: 100%;
    object-fit: cover;

    &[data-loading],
    &[data-error] {
      visibility: hidden;
    }
  }

  span.fallback:not([hidden]) {
    display: block grid;
    font-size: 0.85em;
    grid-area: 1 / 1;
    letter-spacing: 0.02em;
    place-items: center;
  }

  svg {
    block-size: 65%;
    inline-size: 65%;
    opacity: 0.9;
  }
}

@scope (.loam-Avatar-group) to ([class*="loam-"]) {
  :scope {
    align-items: center;
    display: inline flex;
    list-style: none;
    margin: 0;
    padding: 0;
  }

  li {
    border: 2px solid var(--loam-color-surface);
    border-radius: var(--loam-radius-full);
    display: block flex;
    margin: 0;
    margin-inline-start: calc(-1 * var(--loam-avatar-overlap, 0.5rem));
    translate: 0 0;

    &:first-child {
      margin-inline-start: 0;
    }

    @media (prefers-reduced-motion: no-preference) {
      transition: translate var(--loam-duration-md) var(--loam-ease);
    }

    @media (hover: hover) {
      &:hover {
        translate: 0 -2px;
        z-index: 1;
      }
    }
  }
}
}

/* Badge */
@layer loamui.components {
/*
 * The pill is mixed down from one --_color channel: a soft tint toward
 * the page background, with the text pushed toward black or white so the
 * label always reads on its own tint. The accent is the region's.
 */
@scope (.loam-Badge) to ([class*="loam-"]) {
  :scope {
    --_color: var(--loam-color-fg);

    align-items: center;

    /* Tints mix in oklab (cartesian), not oklch: hue interpolation against
       the near-achromatic page background rotates the channel's hue (the
       declared hue of an achromatic colour still interpolates); cartesian
       mixing only desaturates, so the channel's hue survives. */
    background: color-mix(in oklab, var(--_color), var(--loam-color-bg) 85%);

    /* Geometry in em on the size's type step, so a size is one font-size
       decision and the pill scales with the fluid scale. */
    block-size: var(--_h, 1.85em);
    border-radius: var(--_radius, var(--loam-radius-full));
    color: light-dark(
      color-mix(in oklab, var(--_color) 70%, oklch(0% 0 0deg)),
      color-mix(in oklab, var(--_color) 55%, oklch(100% 0 0deg))
    );
    display: inline flex;
    font-size: var(--_fz, var(--loam-text-xs));
    font-weight: 600;
    gap: var(--_gap, var(--loam-space-3xs));
    inline-size: fit-content;
    letter-spacing: 0.01em;
    line-height: 1;
    padding-inline: var(--_px, 0.85em);
    text-decoration: none;
    user-select: none;
    white-space: nowrap;

    &[data-size="sm"] {
      --_h: 1.5em;
      --_px: 0.67em;
    }

    &[data-size="lg"] {
      --_fz: var(--loam-text-sm);
      --_gap: var(--loam-space-2xs);
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        background var(--loam-duration-md) var(--loam-ease),
        color var(--loam-duration-md) var(--loam-ease);
    }
  }

  /* A real element for the label, so it can shrink and truncate; bare text in
     the flex row is an anonymous item and can do neither. */
  span.text {
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  :scope:has(svg) {
    gap: var(--_gap, var(--loam-space-3xs));

    svg {
      block-size: auto;
      flex: none;
      inline-size: 1em;
    }
  }

  /* Context: within a status region — the channel takes the status colour.
     (Even though the region also remaps --loam-color-primary, the badge keys its
     own channel because its rest state is neutral text, not primary.) */
  @container (style(--loam-context: primary)) {
    :scope {
      --_color: var(--loam-color-primary);
    }
  }

  @container (style(--loam-context: success)) {
    :scope {
      --_color: var(--loam-color-success);
    }
  }

  @container (style(--loam-context: warning)) {
    :scope {
      --_color: var(--loam-color-warning);
    }
  }

  @container (style(--loam-context: info)) {
    :scope {
      --_color: var(--loam-color-info);
    }
  }

  @container (style(--loam-context: danger)) {
    :scope {
      --_color: var(--loam-color-danger);
    }
  }

  /* Optical trim: cap-to-alphabetic, so the label centres on the glyphs
     rather than the line box — the pill case where half-leading shows. */
  @supports (text-box: trim-both cap alphabetic) {
    :scope {
      text-box: trim-both cap alphabetic;
    }
  }
}
}

/* Breadcrumbs */
@layer loamui.components {
@scope (.loam-Breadcrumbs) to ([class*="loam-"]) {
  :scope {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);

    ol {
      align-items: center;
      display: block flex;
      flex-wrap: wrap;
      gap: var(--loam-space-2xs);
      list-style: none;
      margin: 0;
      padding: 0;
    }
  }
}

/*
 * Items are scoped to their own root: an <li class="loam-Breadcrumbs-item"> is
 * rendered by the standalone Item component, without the root class on it.
 */
@scope (.loam-Breadcrumbs-item) to ([class*="loam-"]) {
  :scope {
    align-items: center;
    display: inline flex;
    margin-block: 0;
    min-inline-size: 0;

    &:not(:first-child)::before {
      color: var(--loam-color-fg-dim);

      /* The empty alt keeps the glyph out of the accessibility tree. */
      content: var(--loam-breadcrumbs-separator, "/") / "";
      margin-inline-end: var(--loam-space-2xs);
      user-select: none;
    }

    a {
      border-radius: var(--loam-radius-sm);
      color: var(--loam-color-fg-muted);
      padding-inline: 0.125em;
      text-decoration: none;

      @media (prefers-reduced-motion: no-preference) {
        transition: color var(--loam-duration-md) var(--loam-ease);
      }

      @media (hover: hover) {
        &:hover {
          color: var(--loam-color-primary);
          text-decoration: underline;
        }
      }
    }

    &[data-current] {
      color: var(--loam-color-fg);
      font-weight: 600;
    }
  }
}
}

/* Button */
@layer loamui.components {
/*
 * Button — one colour channel (--_color); every look derives from it via
 * color-mix and relative colour. Appearance is decided by context, not
 * props. The one instance-level escape hatch is the public
 * --loam-button-color property (registered in elements.css).
 */
@scope (.loam-Button) to ([class*="loam-"]) {
  :scope {
    --_color: var(--loam-button-color, var(--loam-color-fg));

    background: light-dark(
      color-mix(in oklab, var(--_color), var(--loam-color-bg) 90%),
      color-mix(in oklab, var(--_color), var(--loam-color-bg) 75%)
    );

    /* Tints mix in oklab (cartesian), not oklch: hue interpolation against
       the near-achromatic page background rotates the channel's hue;
       cartesian mixing only desaturates, so the hue survives. */
    border: 1px solid color-mix(in oklab, var(--_color), var(--loam-color-bg) 80%);
    border-radius: var(--loam-button-radius, var(--loam-radius-md));
    box-shadow: var(--loam-shadow-raised);

    /* Text is the channel's hue mixed for AA on the tint, the same
       recipe as Badge and Alert; raw --_color sat below 4.5:1 on its
       own background in six of eight scheme/status combinations. */
    color: light-dark(
      color-mix(in oklab, var(--_color) 70%, oklch(0% 0 0deg)),
      color-mix(in oklab, var(--_color) 55%, oklch(100% 0 0deg))
    );
    display: inline flow-root;
    font-size: var(--loam-text-md);
    font-weight: 600;
    line-height: 1.5;
    padding-block: var(--loam-space-xs);
    padding-inline: var(--loam-space-s);
    text-align: center;
    text-decoration: none;

    /* Identity at rest — the pressed shift transitions both ways. */
    translate: 0 0;
    user-select: none;
    white-space: normal;

    &:active:not(:disabled) {
      background: light-dark(
        color-mix(in oklab, var(--_color), var(--loam-color-bg) 85%),
        color-mix(in oklab, var(--_color), var(--loam-color-bg) 70%)
      );
      box-shadow: var(--loam-shadow-sm);
      color: light-dark(
        oklch(from var(--_color) calc(l - 0.2) c h),
        oklch(from var(--_color) calc(l + 0.2) c h)
      );
      translate: 0 1px;
    }

    &:disabled {
      opacity: var(--loam-disabled-opacity);
    }

    @media (hover: hover) {
      &:hover:not(:disabled) {
        background: light-dark(
          color-mix(in oklab, var(--_color), var(--loam-color-bg) 85%),
          color-mix(in oklab, var(--_color), var(--loam-color-bg) 70%)
        );
        color: light-dark(
          oklch(from var(--_color) calc(l - 0.2) c h),
          oklch(from var(--_color) calc(l + 0.2) c h)
        );
      }
    }

    /* The pressed shift and shadow drop are motion — opt-in only. */
    @media (prefers-reduced-motion: no-preference) {
      transition:
        background var(--loam-duration-sm) var(--loam-ease),
        border-color var(--loam-duration-sm) var(--loam-ease),
        color var(--loam-duration-sm) var(--loam-ease),
        box-shadow var(--loam-duration-md) var(--loam-ease),
        translate var(--loam-duration-md) var(--loam-ease);
    }
  }

  /* A composed Loader is an icon here: the same detection, layout and 1em
     sizing (answered through the loader's inherited size hook). */
  :scope:has(svg, .loam-Loader) {
    --loam-loader-size: 1em;

    align-items: center;
    display: inline flex;
    gap: var(--loam-space-2xs);
    justify-content: center;

    svg {
      block-size: auto;
      inline-size: 1em;
    }
  }

  /* Icon-only is detected from the accessible name, not DOM shape: an
     icon-only button MUST be named — aria-label, aria-labelledby, or hidden
     text beside the icon — and `svg:only-child` can't tell "icon + bare
     text" from "icon alone" (text nodes aren't elements). */
  :scope:has(svg, .loam-Loader):is(
      [aria-label],
      [aria-labelledby],
      :has(> :is(svg, .loam-Loader):first-child + .loam-VisuallyHidden:last-child),
      :has(> .loam-VisuallyHidden:first-child + :is(svg, .loam-Loader):last-child)
    ) {
    min-block-size: calc(1.5em + var(--loam-space-xs) * 2 + 2px);
    min-inline-size: calc(1.5em + var(--loam-space-xs) * 2 + 2px);
    padding-inline: var(--loam-space-xs);
  }

  /* Context: within a primary region — THE action of the area. */
  @container (style(--loam-context: primary)) {
    :scope {
      --_color: var(--loam-color-primary);
    }
  }

  /* Context: within a status region. (The region also re-answers the semantic
     tokens for every other component — see the Contexts section of tokens.css;
     these rules key the button's own channel.) */
  @container (style(--loam-context: danger)) {
    :scope {
      --_color: var(--loam-color-danger);
    }
  }

  @container (style(--loam-context: success)) {
    :scope {
      --_color: var(--loam-color-success);
    }
  }

  @container (style(--loam-context: warning)) {
    :scope {
      --_color: var(--loam-color-warning);
    }
  }

  @container (style(--loam-context: info)) {
    :scope {
      --_color: var(--loam-color-info);
    }
  }

  @container (inline-size <= 16rem) {
    :scope {
      inline-size: 100%;
    }
  }
}
}

/* Card */
@layer loamui.components {
@scope (.loam-Card) to ([class*="loam-"]) {
  :scope {
    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line);
    border-radius: var(--_radius, var(--loam-radius-lg));
    display: block flow;
    padding: var(--loam-space-s);
  }
}
}

/* Carousel */
@layer loamui.components {
/* A carousel is a region: it declares its container so the fluid tokens
   answer the carousel's width, and every item hosts consumer content (a
   Card, an image, a quote), hence the donut. Previous and Next are Buttons
   and keep their own recipe behind it. */
@scope (.loam-Carousel) to ([class*="loam-"]) {
  :scope {
    container-type: inline-size;
    display: block grid;
    gap: var(--loam-space-xs);
  }

  /* A column grid inside a scrollport. Every item snaps to the start, and
     a page is one clientWidth, which is what Previous, Next and the arrow
     keys scroll by. Items share one width, the public
     --loam-carousel-item-size, never wider than the track, so a narrow
     container still shows one whole item. The track is in the tab order,
     so the global focus ring marks it; the padding keeps that ring and an
     item's own inside the scrollport. */
  ul.track {
    display: block grid;
    gap: var(--loam-space-xs);
    grid-auto-columns: min(100%, var(--loam-carousel-item-size, 20rem));
    grid-auto-flow: column;
    list-style: none;
    margin: 0;

    /* overflow (shorthand), not overflow-inline: the logical property is
       not yet Baseline, and the track would not scroll in some browsers. */
    overflow: auto;
    overscroll-behavior-inline: contain;
    padding: var(--loam-space-3xs);

    /* A containing block for hidden text inside an item, so it cannot
       escape the scroller and widen the page. */
    position: relative;
    scroll-padding-inline: var(--loam-space-3xs);
    scroll-snap-type: inline mandatory;

    /* A grid, so a Card or an image stretches to the row's height. */
    > li {
      display: block grid;
      margin: 0;
      scroll-snap-align: start;
    }

    @media (prefers-reduced-motion: no-preference) {
      scroll-behavior: smooth;
    }
  }

  /* One dot per item: a glyph control, so its geometry is em on the
     text-sm basis like Radio's and Range's. A 1.75em target (at least
     24px at every container width) around a 0.6em mark, so the pointer
     has something to hit. The current dot is the ARIA state, detected
     rather than duplicated. */
  ul.indicators {
    display: block flex;
    flex-wrap: wrap;
    gap: var(--loam-space-3xs);
    justify-content: center;
    list-style: none;
    margin: 0;
    padding: 0;

    > li {
      margin: 0;
    }

    button {
      background: transparent;
      block-size: 1.75em;
      border: none;
      border-radius: var(--loam-radius-full);
      box-shadow: none;
      display: block grid;
      font-size: var(--loam-text-sm);
      inline-size: 1.75em;
      padding: 0;
      place-items: center;

      &::before {
        background: var(--loam-color-line-strong);
        block-size: 0.6em;
        border-radius: var(--loam-radius-full);
        content: "";
        inline-size: 0.6em;
      }

      &[aria-current]::before {
        background: var(--loam-color-primary-strong);
      }

      @media (prefers-reduced-motion: no-preference) {
        &::before {
          transition:
            background var(--loam-duration-sm) var(--loam-ease),
            scale var(--loam-duration-sm) var(--loam-ease);
        }
      }

      @media (hover: hover) {
        &:hover::before {
          scale: 1.25;
        }
      }
    }
  }

  /* Forced colours: a dot is a painted background and would vanish, so
     each keeps a border, and the current one an outline. */
  @media (forced-colors: active) {
    ul.indicators button {
      &::before {
        border: 1px solid ButtonText;
      }

      &[aria-current]::before {
        outline: 2px solid ButtonText;
        outline-offset: 2px;
      }
    }
  }
}

/* Previous and Next are core Buttons, past the carousel's donut: this
   scope adds the chevron's direction and the unavailable state. The glyph
   is drawn for LTR, where Previous points left, and mirrored where the
   start is on the right, so it points toward the start in both
   directions. At an end the button carries aria-disabled, which Button's
   own :disabled rule cannot see, so the same paint is applied here. */
@scope (.loam-Carousel-control) {
  :scope[aria-disabled="true"] {
    cursor: not-allowed;
    opacity: var(--loam-disabled-opacity);
  }

  svg:dir(rtl) {
    scale: -1 1;
  }

  /* Forced colours flatten opacity, so the unavailable button is named in
     GrayText, as a native disabled button is. */
  @media (forced-colors: active) {
    :scope[aria-disabled="true"] {
      border-color: GrayText;
      color: GrayText;
    }
  }
}
}

/* Checkbox */
@layer loamui.components {
/* The control is a native checkbox — the elements layer paints it with
   accent-color (the neutral primary's audited -strong, so a --loam-context
   region keeps 3:1 under the tick). The component adds only a comfortable
   size, the invalid affordance, and the label row. */
@scope (.loam-Checkbox) to ([class*="loam-"]) {
  :scope {
    accent-color: var(--loam-color-primary-strong);
    block-size: 1.15em;
    flex-shrink: 0;
    inline-size: 1.15em;
    margin: 0;

    /* Invalid is a box-shadow, not an outline, so it stacks with rather
       than replaces the elements-layer :focus-visible ring; the focus ring
       itself turns danger-coloured. Mirrors Switch. */
    &[aria-invalid="true"] {
      --loam-color-ring: var(--loam-color-danger-strong);

      box-shadow: 0 0 0 2px var(--loam-color-danger);
    }
  }

  /* Forced colors drop box-shadow, so the invalid affordance moves to an
     outline in a system colour (the focus ring is offset further out and
     still reads as its own ring). */
  @media (forced-colors: active) {
    :scope[aria-invalid="true"] {
      outline: 2px solid LinkText;
      outline-offset: 1px;

      &:focus-visible {
        outline: var(--loam-ring-width) solid Highlight;
        outline-offset: 2px;
      }
    }
  }
}

/* The labelled form: an inline row with the control beside its text.
   Donut: the label body hosts consumer content. */
@scope (.loam-Checkbox-wrapper) to ([class*="loam-"]) {
  :scope {
    display: block flex;
    flex-direction: column;
    gap: var(--loam-space-3xs);

    &:has(input:disabled) {
      opacity: var(--loam-disabled-opacity);
    }
  }

  label {
    align-items: center;
    cursor: pointer;
    display: block flex;
    gap: var(--loam-space-2xs);
    min-block-size: 2.75rem;

    &:has(input:disabled) {
      cursor: not-allowed;
    }
  }

  span.body {
    display: block flex;
    flex-direction: column;
    gap: 0.125em;
  }

  span.label {
    font-size: var(--loam-text-md);
    font-weight: 500;
  }

  span.description {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-xs);
  }
}
}

/* Combobox */
@layer loamui.components {
/*
 * Combobox — a text box with a listbox of suggestions under it. The box is
 * the library's Input and the optional trigger its Button, behind the
 * donut; the scope arranges the row and paints the list.
 */
@scope (.loam-Combobox) to ([class*="loam-"]) {
  /* One row: the box grows, a Trigger beside it keeps its width. The list
     is out of flow, so it takes no track. */
  :scope {
    display: block grid;
    gap: var(--loam-space-2xs);
    grid-auto-columns: max-content;
    grid-auto-flow: column;
    grid-template-columns: minmax(0, 1fr);
    position: relative;
  }

  /* The list's height is the public --loam-combobox-list-size, capped at
     half the viewport so it never runs off the screen it is tethered to. */
  ul {
    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line);
    border-radius: var(--loam-radius-md);
    box-shadow: var(--loam-shadow-lg);
    list-style: none;
    margin: 0;
    max-block-size: min(var(--loam-combobox-list-size, 16rem), 50vb);
    overflow: auto;
    overscroll-behavior: contain;
    padding: var(--loam-space-3xs);
    z-index: var(--loam-z-popup);

    /* Enhanced: tethered to the native input (which carries the anchor name), as wide as it, flipping above when the viewport runs out. */
    @supports (anchor-name: --loam-probe) {
      inline-size: anchor-size(inline);
      inset: auto;
      inset-block-start: calc(anchor(end) + var(--loam-space-3xs));
      inset-inline-start: anchor(start);
      position: fixed;
      position-try: flip-block;
    }

    /* Fallback: under the whole row, as wide as it. */
    @supports not (anchor-name: --loam-probe) {
      inset-block-start: calc(100% + var(--loam-space-3xs));
      inset-inline: 0;
      position: absolute;
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        opacity var(--loam-duration-md) var(--loam-ease),
        translate var(--loam-duration-md) var(--loam-ease);

      @starting-style {
        opacity: 0;
        translate: 0 -0.25rem;
      }
    }
  }

  li {
    margin: 0;
  }

  [role="option"] {
    border-radius: var(--loam-radius-sm);
    cursor: default;
    font-size: var(--loam-text-sm);
    padding-block: var(--loam-space-2xs);
    padding-inline: var(--loam-space-2xs);
    user-select: none;

    /* The highlight follows the keyboard and the pointer alike; focus
       itself never leaves the box (aria-activedescendant). */
    &[data-highlighted] {
      background: var(--loam-color-primary-soft);
    }

    &[aria-selected="true"] {
      font-weight: 600;
    }

    &[aria-disabled="true"] {
      color: var(--loam-color-fg-dim);
    }

    @media (prefers-reduced-motion: no-preference) {
      transition: background var(--loam-duration-sm) var(--loam-ease);
    }
  }

  li.empty {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);
    padding-block: var(--loam-space-2xs);
    padding-inline: var(--loam-space-2xs);
  }

  /* Forced colours: the highlight is background-only, and aria-disabled
     (unlike :disabled) gets no automatic GrayText. */
  @media (forced-colors: active) {
    [role="option"][data-highlighted] {
      background: Highlight;
      color: HighlightText;
      forced-color-adjust: none;
    }

    [role="option"][aria-disabled="true"] {
      color: GrayText;
    }
  }
}
}

/* CopyButton */
@layer loamui.components {
/*
 * CopyButton — a Button that says what it did. The scope root is a span
 * with no box (display: contents), so the Button inside takes part in the
 * parent's layout exactly as a bare Button would: a grid region stretches
 * it, a narrow container makes it full width. The donut stops at
 * .loam-Button, so nothing here restyles the Button.
 */
@scope (.loam-CopyButton) to ([class*="loam-"]) {
  :scope {
    display: contents;
  }
}
}

/* DateInput */
@layer loamui.components {
/* The legend comes from the Fieldset primitive and each part is a core
   Field around a core Input, so labels, borders, focus rings and the
   aria-invalid danger state are theirs. Only the description and error
   text and the row of parts live here; the donut stops this scope at the
   composed Fields. */
@scope (.loam-DateInput) to ([class*="loam-"]) {
  p.description {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);
    margin: 0;
  }

  p.error {
    color: var(--loam-color-danger);
    font-size: var(--loam-text-sm);
    margin: 0;
  }

  /* A grid of max-content columns, not a flex row: a Field fills its
     container, and a max-content track is exactly as wide as the sized
     Input inside it, so each part is as wide as its answer without any
     rule reaching past the donut. */
  div.parts {
    display: block grid;
    gap: var(--loam-space-xs);
    grid-auto-columns: max-content;
    grid-auto-flow: column;
    justify-content: start;
  }
}
}

/* Details */
@layer loamui.components {
/* The native disclosure, standalone: each Details is its own boxed
   surface. A set sharing a `name` needs no wrapper — the browser owns
   the exclusivity. */
@scope (.loam-Details) to ([class*="loam-"]) {
  :scope {
    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line);
    border-radius: var(--loam-radius-md);
    inline-size: 100%;
    overflow: clip;

    > summary {
      align-items: center;
      cursor: pointer;
      display: block flex;
      font-size: var(--loam-text-md);
      font-weight: 600;
      gap: var(--loam-space-xs);
      justify-content: space-between;
      line-height: 1.4;
      list-style: none;
      padding-block: var(--loam-space-s);
      padding-inline: var(--loam-space-s);
      user-select: none;

      @media (prefers-reduced-motion: no-preference) {
        transition: background var(--loam-duration-md) var(--loam-ease);
      }
    }
  }

  :scope > summary::-webkit-details-marker {
    display: none;
  }

  /* -strong, not raw primary: summary text must hold 4.5:1 inside a
     --loam-context region, where raw warning could not. */
  :scope[open] > summary {
    color: var(--loam-color-primary-strong);
  }

  span.label {
    min-inline-size: 0;
  }

  /* Sized in CSS, not SVG attributes, so the chevron rides the summary's
     font size. */
  svg.chevron {
    block-size: 1em;
    color: var(--loam-color-fg-muted);
    flex: none;
    inline-size: 1em;
    rotate: 0deg;

    :scope[open] > summary & {
      color: var(--loam-color-primary-strong);
      rotate: 180deg;
    }

    @media (prefers-reduced-motion: no-preference) {
      transition: rotate var(--loam-duration-md) var(--loam-ease);
    }
  }

  div.content {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);
    line-height: 1.55;
    padding-block: var(--loam-space-s);
    padding-inline: var(--loam-space-s);
  }

  @media (hover: hover) {
    :scope > summary:hover {
      background: var(--loam-color-surface-hover);
    }
  }

  /* Open/close height animation on the platform's own content slot.
     interpolate-size is required for the 0 → auto interpolation and is not
     yet Baseline, so the whole recipe gates on it (elsewhere: today's
     instant toggle). Declared on the item, never :root — a library must not
     leak keyword interpolation into the host page. */
  @supports (interpolate-size: allow-keywords) {
    :scope {
      interpolate-size: allow-keywords;
    }

    @media (prefers-reduced-motion: no-preference) {
      :scope::details-content {
        block-size: 0;
        overflow-block: clip;
        transition:
          block-size var(--loam-duration-md) var(--loam-ease),
          content-visibility var(--loam-duration-md) var(--loam-ease) allow-discrete;
      }

      :scope[open]::details-content {
        block-size: auto;
      }
    }
  }
}
}

/* Drawer */
@layer loamui.components {
@scope (.loam-Drawer-popup) to ([class*="loam-"]) {
  :scope {
    /* Panel extent on the short axis (width for start/end, height for
       top/bottom). Block children stretch, so content can't decide this one —
       the consumer's layout does, through the public property. */
    --_size: var(--loam-drawer-size, 24rem);
    --_hidden: 0 0;

    background: var(--loam-color-surface);
    border: 0;
    box-shadow: var(--loam-shadow-lg);
    font-size: var(--loam-text-sm);
    margin: 0;
    overflow: auto;
    overscroll-behavior: contain;
    padding: var(--loam-space-s);

    &[data-side="start"],
    &[data-side="end"] {
      block-size: auto;
      inline-size: min(var(--_size), 100dvi - var(--loam-space-l));
      inset-block: 0;
      max-block-size: 100dvb;
    }

    &[data-side="start"] {
      --_hidden: -100% 0;

      border-end-end-radius: var(--loam-radius-lg);
      border-start-end-radius: var(--loam-radius-lg);
      inset-inline: 0 auto;
    }

    &[data-side="end"] {
      --_hidden: 100% 0;

      border-end-start-radius: var(--loam-radius-lg);
      border-start-start-radius: var(--loam-radius-lg);
      inset-inline: auto 0;
    }

    &[data-side="top"],
    &[data-side="bottom"] {
      block-size: min(var(--_size), 100dvb - var(--loam-space-l));
      inline-size: auto;
      inset-inline: 0;
      max-inline-size: 100dvi;
    }

    &[data-side="top"] {
      --_hidden: 0 -100%;

      border-end-end-radius: var(--loam-radius-lg);
      border-end-start-radius: var(--loam-radius-lg);
      inset-block: 0 auto;
    }

    &[data-side="bottom"] {
      --_hidden: 0 100%;

      border-start-end-radius: var(--loam-radius-lg);
      border-start-start-radius: var(--loam-radius-lg);
      inset-block: auto 0;
    }

    &::backdrop {
      background: color-mix(in oklch, var(--loam-color-fg) 45%, transparent);
    }

    @media (forced-colors: active) {
      border: 1px solid;
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        translate var(--loam-duration-lg) var(--loam-ease),
        display var(--loam-duration-lg) var(--loam-ease) allow-discrete,
        overlay var(--loam-duration-lg) var(--loam-ease) allow-discrete;
      translate: 0 0;

      &:not([open]) {
        translate: var(--_hidden);
      }

      &::backdrop {
        opacity: 1;
        transition:
          opacity var(--loam-duration-lg) var(--loam-ease),
          display var(--loam-duration-lg) var(--loam-ease) allow-discrete,
          overlay var(--loam-duration-lg) var(--loam-ease) allow-discrete;

        @starting-style {
          opacity: 0;
        }
      }

      &:not([open])::backdrop {
        opacity: 0;
      }

      @starting-style {
        translate: var(--_hidden);
      }
    }
  }

  h2.title {
    font-size: var(--loam-text-lg);
    font-weight: 600;
    margin-block: 0 var(--loam-space-2xs);
  }

  p.description {
    color: var(--loam-color-fg-muted);
    margin-block: 0 var(--loam-space-xs);
  }
}
}

/* ErrorSummary */
@layer loamui.components {
@scope (.loam-ErrorSummary) to ([class*="loam-"]) {
  :scope {
    background: var(--loam-color-danger-soft);
    border: 2px solid var(--loam-color-danger);
    border-radius: var(--loam-radius-md);
    padding: var(--loam-space-s);

    &:focus-visible {
      outline: var(--loam-ring-width) solid var(--loam-color-danger-strong);
      outline-offset: 2px;
    }
  }

  h2 {
    font-size: var(--loam-text-md);
    font-weight: 700;
    margin: 0;
    text-wrap: balance;
  }

  li {
    margin-block: 0;
  }

  ul {
    display: block flex;
    flex-direction: column;
    gap: var(--loam-space-3xs);
    margin-block: var(--loam-space-2xs) 0;
    margin-inline: 0;
    padding-inline-start: var(--loam-space-s);
  }

  a {
    color: light-dark(
      color-mix(in oklab, var(--loam-color-danger) 70%, oklch(0% 0 0deg)),
      color-mix(in oklab, var(--loam-color-danger) 55%, oklch(100% 0 0deg))
    );
    font-size: var(--loam-text-sm);
    font-weight: 600;
    text-decoration: underline;
    text-decoration-skip-ink: auto;
  }
}
}

/* Field */
@layer loamui.components {
/*
 * Field — parts are targeted from within the @scope; the root keeps a
 * `loam-` class so the scope root is unambiguous and app styles can find it.
 * The scope stops at any nested loam-classed element: the field hosts
 * controls it doesn't own (a Checkbox has its own label), and the donut
 * keeps these type selectors on the field's own parts only.
 */
@scope (.loam-Field) to ([class*="loam-"]) {
  :scope {
    display: block flex;
    flex-direction: column;
    gap: var(--loam-space-3xs);
  }
}

/*
 * The parts carry their own roots, so a label, description or error keeps
 * its look wherever a composition places it: a composition's loam- class
 * is a scoping limit for the field's scope, and without these a Field
 * inside a SettingRow or PasswordField would lose its type. The field's
 * scope above keeps only the stack.
 */
@scope (.loam-Field-label) {
  :scope {
    align-items: center;
    display: block flex;
    font-size: var(--loam-text-sm);
    font-weight: 500;
    gap: var(--loam-space-2xs);

    &:has(input:is([type="checkbox"], [type="radio"])) {
      cursor: pointer;
      min-block-size: 2.75rem;

      &:has(input:disabled) {
        cursor: not-allowed;
      }
    }
  }

  span.optional {
    color: var(--loam-color-fg-muted);
    font-weight: 400;
  }
}

@scope (.loam-Field-description) {
  :scope {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);
    margin: 0;
  }
}

@scope (.loam-Field-error) {
  :scope {
    color: var(--loam-color-danger);
    font-size: var(--loam-text-sm);
    margin: 0;
  }
}
}

/* Fieldset */
@layer loamui.components {
/*
 * Fieldset — native <fieldset>/<legend>, styled with @scope. The browser's
 * default fieldset border/margin/padding are reset so it composes cleanly.
 */
@scope (.loam-Fieldset) to ([class*="loam-"]) {
  :scope {
    border: 0;
    display: block flex;
    flex-direction: column;
    gap: var(--loam-space-2xs);
    margin: 0;
    min-inline-size: 0;
    padding: 0;
  }

  legend {
    font-size: var(--loam-text-sm);
    font-weight: 500;
    padding: 0;
  }

  span.optional {
    color: var(--loam-color-fg-muted);
    font-weight: 400;
  }
}
}

/* FileInput */
@layer loamui.components {
/* The native file input is the control; the Prompt label is the box. The
   Root is the drop target and hosts the list, so every part is a type
   selector inside it. Donut: the label hosts consumer text. */
@scope (.loam-FileInput) to ([class*="loam-"]) {
  :scope {
    inline-size: 100%;
    position: relative;
  }

  label {
    background: var(--loam-color-surface);
    border: 1px dashed var(--loam-color-line-strong);
    border-radius: var(--loam-radius-lg);
    color: var(--loam-color-fg-muted);
    cursor: pointer;
    display: block flow;
    font-size: var(--loam-text-sm);
    padding: var(--loam-space-s);
    text-align: center;

    /* A file held over the box: the -strong border holds 3:1 on the soft
       tint in every context. */
    :scope[data-dragging] & {
      background: var(--loam-color-primary-soft);
      border-color: var(--loam-color-primary-strong);
      color: var(--loam-color-fg);
    }

    /* The ring is drawn on the box, whose input is visually hidden but
       keeps focus; danger re-answers the ring the way Input does. */
    :scope:has(input:focus-visible) & {
      border-color: var(--loam-color-primary);
      outline: var(--loam-ring-width) solid var(--loam-color-ring);
      outline-offset: 2px;
    }

    :scope:has(input[aria-invalid="true"]) & {
      --loam-color-ring: var(--loam-color-danger-strong);

      border-color: var(--loam-color-danger);
    }

    :scope:has(input:disabled) & {
      cursor: not-allowed;
      opacity: var(--loam-disabled-opacity);
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        background var(--loam-duration-sm) var(--loam-ease),
        border-color var(--loam-duration-sm) var(--loam-ease);
    }
  }

  /* The native control inside a Root wears .loam-VisuallyHidden (set by
     the component): out of sight, still focusable and in the
     accessibility tree, the box doing the inviting. */

  ul {
    font-size: var(--loam-text-sm);
    margin: 0;

    /* No display: none while empty — a live region must already be in the
       accessibility tree when its content changes for the change to be
       announced. The list simply has no height until it has items. */
    &:not(:empty) {
      margin-block-start: var(--loam-space-2xs);
    }
  }

  li {
    overflow-wrap: anywhere;
  }

  span.size {
    color: var(--loam-color-fg-muted);
  }

  /* Dashed borders and outlines survive forced colours; the state and the
     ring move to system colours. */
  @media (forced-colors: active) {
    label {
      border-color: CanvasText;

      :scope[data-dragging] & {
        border-color: Highlight;
        border-style: solid;
      }

      :scope:has(input:focus-visible) & {
        outline-color: Highlight;
      }
    }
  }
}
}

/* Input */
@layer loamui.components {
@scope (.loam-Input-field) to ([class*="loam-"]) {
  :scope {
    /* The inner control's curve, derived here because --_radius does not
       inherit: computed on the root, handed down as an ordinary property. */
    --_inner-radius: calc(var(--_radius, var(--loam-radius-md)) - 1px);

    align-items: center;
    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line-strong);
    border-radius: var(--_radius, var(--loam-radius-md));
    display: block flex;
    font-size: var(--loam-text-md);
    gap: var(--loam-space-2xs);
    line-height: 1.5;

    &:focus-within {
      border-color: var(--loam-color-primary);
      outline: var(--loam-ring-width) solid var(--loam-color-ring);
      outline-offset: 2px;
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        border-color var(--loam-duration-sm) var(--loam-ease),
        box-shadow var(--loam-duration-md) var(--loam-ease);
    }
  }

  /* A sized input is as wide as its answer: the box shrink-wraps the
     platform's own width for `size` characters instead of filling the
     field. */
  :scope:has(> input[size]) {
    inline-size: fit-content;
  }

  /* Autofilled: the wrapper border carries the signal (the input itself
     paints the tint — see below). :autofill and :-webkit-autofill must stay
     in separate blocks — one unsupported selector invalidates a shared
     list. Declared before the error state so danger wins at equal
     specificity. */
  :scope:has(input:autofill) {
    border-color: color-mix(in oklab, var(--loam-color-primary) 50%, var(--loam-color-line-strong));
  }

  :scope:has(input:-webkit-autofill) {
    border-color: color-mix(in oklab, var(--loam-color-primary) 50%, var(--loam-color-line-strong));
  }

  /* Invalid after a composed Field.Error or a native submit attempt. */
  :scope:has(input[aria-invalid="true"]) {
    --loam-color-ring: var(--loam-color-danger-strong);

    border-color: var(--loam-color-danger);
  }

  :scope:has(input:disabled) {
    background: var(--loam-color-bg-subtle);
    opacity: var(--loam-disabled-opacity);
  }

  input {
    background: transparent;
    border: none;

    /* Follows the wrapper's inner curve, one border-width tighter (see the
       root). Read from the inherited private property, not from --_radius:
       that one is registered inherits: false, so a child always falls back to
       radius-md and would keep the wrong curve the moment a consumer set it. */
    border-radius: var(--_inner-radius);
    color: inherit;
    font: inherit;
    min-inline-size: 0;
    outline: none;

    /* The native control owns its padding, so the whole text-entry area
       accepts taps. Body type and spacing share Button's derived height. */
    padding: var(--loam-space-xs);
  }

  /* Without `size` the input takes the box; with it, the platform's own
     width for that many characters stands. */
  input:not([size]) {
    flex: 1;
  }

  input[size] {
    flex: none;
  }

  input::placeholder {
    color: var(--loam-color-fg-dim);
  }

  /* background-color cannot be overridden on an autofilled field; an inset
     shadow wider than the control paints the tint instead. Separate blocks —
     see the wrapper rules above. */
  input:autofill {
    box-shadow: inset 0 0 0 100px
      color-mix(in oklab, var(--loam-color-primary) 8%, var(--loam-color-surface));
  }

  input:-webkit-autofill {
    box-shadow: inset 0 0 0 100px
      color-mix(in oklab, var(--loam-color-primary) 8%, var(--loam-color-surface));
  }

  span.section {
    align-items: center;
    color: var(--loam-color-fg-muted);
    display: inline flex;
    flex-shrink: 0;

    &:first-child {
      padding-inline-start: var(--loam-space-xs);
    }

    &:last-child {
      padding-inline-end: var(--loam-space-xs);
    }

    /* Sections must not stretch the derived height. */
    svg {
      block-size: auto;
      inline-size: 1.125em;
    }
  }

  /* Forced colours drop the danger border colour, so the invalid state
     moves to an outline in a system colour; the focus ring is offset
     further out and still reads as its own ring. Mirrors Checkbox. */
  @media (forced-colors: active) {
    :scope:has(input[aria-invalid="true"]) {
      outline: 2px solid LinkText;
      outline-offset: 1px;

      &:focus-within {
        outline: var(--loam-ring-width) solid Highlight;
        outline-offset: 2px;
      }
    }
  }
}
}

/* Loader */
@layer loamui.components {
@scope (.loam-Loader) {
  :scope {
    /* The hook is inherited, so a composing component can size a loader
       from its own context (Button answers it with the same 1em its
       icons get) without fighting this default's specificity. */
    --_size: var(--loam-loader-size, 1.5rem);

    align-items: center;
    block-size: var(--_size);

    /* The accent — a loader is a pure indicator, and indicators carry the
       chromatic flourish while everything else stays neutral. A context
       region still recolours it (context remaps the accent too). */
    color: var(--loam-color-accent);
    display: inline flex;
    inline-size: var(--_size);
    justify-content: center;
    vertical-align: middle;

    /* An explicit size out-ranks the inherited hook: the attribute is only
       emitted when the consumer asked for one. */
    &[data-size="sm"] {
      --_size: 1rem;
    }

    &[data-size="md"] {
      --_size: 1.5rem;
    }

    &[data-size="lg"] {
      --_size: 2.25rem;
    }
  }

  span.spinner {
    block-size: 100%;
    border: max(2px, calc(var(--_size) * 0.12)) solid
      color-mix(in oklch, currentcolor 25%, transparent);
    border-block-start-color: currentcolor;
    border-radius: var(--loam-radius-full);
    inline-size: 100%;

    @media (prefers-reduced-motion: no-preference) {
      animation: loam-loader-spin calc(var(--loam-duration-lg) * 2) linear infinite;
    }

    /* Forced colors flatten the 25% ring and the solid edge to one colour,
       so the rotation reads as a static circle; keep the open ring explicit. */
    @media (forced-colors: active) {
      border-color: CanvasText transparent transparent;
      forced-color-adjust: none;
    }
  }
}

@keyframes loam-loader-spin {
  to {
    transform: rotate(1turn);
  }
}
}

/* Menu */
@layer loamui.components {
@scope (.loam-Menu) {
  :scope {
    display: inline flow-root;
    position: relative;
  }
}

/* The popup escapes the wrapper into the top layer when enhanced, so it
   carries its own scope (same engine as Popover). */
@scope (.loam-Menu-popup) to ([class*="loam-"]) {
  :scope {
    /* Content decides the width between a floor and a cap; the cap is the
       public `--loam-menu-size`, the floor derives from it. */
    --_size: var(--loam-menu-size, 18rem);

    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line);
    border-radius: var(--loam-radius-md);
    box-shadow: var(--loam-shadow-lg);
    inline-size: max-content;
    margin: 0;
    max-inline-size: min(var(--_size), 90vi);
    min-inline-size: calc(var(--_size) * 0.6);
    padding: var(--loam-space-3xs);

    /* Enhanced: top layer + anchor positioning, tethered to the trigger. */
    @supports (anchor-name: --loam-probe) {
      /* The UA gives [popover] `inset: 0`; clear it so only the insets each
         branch sets take part, otherwise the box is over-constrained. */
      inset: auto;
      inset-inline-start: anchor(start);
      position: fixed;
      position-try: flip-block;

      &[data-side="bottom"] {
        inset-block-start: calc(anchor(end) + var(--loam-space-3xs));
      }

      &[data-side="top"] {
        inset-block-end: calc(anchor(start) + var(--loam-space-3xs));
      }

      @media (prefers-reduced-motion: no-preference) {
        /* -md where Popover uses -lg: intentional — menus should feel
           snappier than panels. */
        opacity: 1;
        transition:
          opacity var(--loam-duration-md) var(--loam-ease),
          translate var(--loam-duration-md) var(--loam-ease),
          display var(--loam-duration-md) var(--loam-ease) allow-discrete,
          overlay var(--loam-duration-md) var(--loam-ease) allow-discrete;
        translate: 0 0;

        /* Exit: transition back to the closed state (see Modal). */
        &:not(:popover-open) {
          opacity: 0;
          translate: 0 -0.25rem;
        }

        &[data-side="top"] {
          &:not(:popover-open) {
            translate: 0 0.25rem;
          }

          @starting-style {
            translate: 0 0.25rem;
          }
        }

        @starting-style {
          opacity: 0;
          translate: 0 -0.25rem;
        }
      }
    }

    /* Fallback: no anchor positioning (and no top layer) — anchor to the
       wrapper as a normal absolutely-positioned panel. */
    @supports not (anchor-name: --loam-probe) {
      inset-inline-start: 0;
      position: absolute;
      z-index: var(--loam-z-popup);

      &[data-side="bottom"] {
        inset-block-start: calc(100% + var(--loam-space-3xs));
      }

      &[data-side="top"] {
        inset-block-end: calc(100% + var(--loam-space-3xs));
      }
    }
  }

  .item {
    align-items: center;
    background: none;
    border: 0;
    border-radius: var(--loam-radius-sm);
    box-shadow: none;
    color: var(--loam-color-fg);
    cursor: default;
    display: block flex;
    font: inherit;
    font-size: var(--loam-text-sm);
    gap: var(--loam-space-2xs);
    inline-size: 100%;

    /* Focus IS the highlight in a menu (roving focus, APG pattern); the
       elements-layer ring is for tabbable controls, not menu items. */
    outline: none;
    padding-block: var(--loam-space-2xs);
    padding-inline: var(--loam-space-2xs);
    text-align: start;
    text-decoration: none;
    user-select: none;
    white-space: nowrap;

    &:focus:not([aria-disabled="true"]) {
      background: var(--loam-color-primary-soft);
    }

    &[aria-disabled="true"] {
      color: var(--loam-color-fg-dim);
    }

    svg {
      block-size: auto;
      flex-shrink: 0;
      inline-size: 1em;
    }

    /* Checkable items keep a glyph cell whether or not they are checked, so
       labels line up; the glyph itself is cut from currentColor. */
    &[role="menuitemcheckbox"]::before,
    &[role="menuitemradio"]::before {
      block-size: 1em;
      content: "";
      flex-shrink: 0;
      inline-size: 1em;
    }

    &[role="menuitemcheckbox"][aria-checked="true"]::before {
      background: currentcolor;
      clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%);
    }

    &[role="menuitemradio"][aria-checked="true"]::before {
      background: currentcolor;
      clip-path: circle(25%);
    }

    @media (prefers-reduced-motion: no-preference) {
      transition: background var(--loam-duration-sm) var(--loam-ease);
    }

    @media (hover: hover) {
      &:hover:not([aria-disabled="true"]) {
        background: var(--loam-color-primary-soft);
      }
    }
  }

  div.group-label {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
    padding-block: var(--loam-space-3xs);
    padding-inline: var(--loam-space-2xs);
    text-transform: uppercase;
  }

  hr {
    border: 0;
    border-block-start: 1px solid var(--loam-color-line);
    margin-block: var(--loam-space-3xs);
    margin-inline: calc(-1 * var(--loam-space-3xs));
  }

  /* Forced colors: the roving highlight is background-only, and
     aria-disabled (unlike :disabled) gets no automatic GrayText. */
  @media (forced-colors: active) {
    .item:focus:not([aria-disabled="true"]),
    .item:hover:not([aria-disabled="true"]) {
      background: Highlight;
      color: HighlightText;
      forced-color-adjust: none;
    }

    .item[aria-disabled="true"] {
      color: GrayText;
    }

    /* The glyph is a background cut to shape; keep it painted in the
       item's own forced colour. */
    .item[aria-checked="true"]::before {
      forced-color-adjust: none;
    }
  }
}
}

/* Meter */
@layer loamui.components {
/* The scope root is the native <meter> itself. `appearance: none` opts out
   of the UA painting, so the element is the track and the vendor
   pseudo-elements carry the fill — each browser names them differently,
   so each gets its own rule. */
@scope (.loam-Meter) to ([class*="loam-"]) {
  :scope {
    --_h: 0.625rem;

    /* The browser decides the band from low/high/optimum; the CSS only
       answers each band with a colour. Fills are the -strong family so
       they hold 3:1 on the subtle track in both schemes. Without bands
       every value is "optimum", and the fill IS the primary token — a
       `--loam-context` region re-answers it (see the Contexts section of
       tokens.css), so status needs no component code here. */
    --_optimum: var(--loam-color-primary-strong);

    /* Firefox names the band with a pseudo-class on the element, so its
       one fill pseudo-element reads the answer from here. */
    --_fill: var(--_optimum);

    appearance: none;
    background: var(--loam-color-bg-subtle);
    block-size: var(--_h);
    border: none;
    border-radius: var(--loam-radius-full);
    display: block flow;
    inline-size: 100%;
    overflow: clip;

    &[data-size="sm"] {
      --_h: 0.375rem;
    }

    &[data-size="lg"] {
      --_h: 1rem;
    }

    /* With bands declared, the good band is a status of its own (success),
       not the surrounding context: the bands are the meter's judgment. */
    &:is([low], [high]) {
      --_optimum: var(--loam-color-success-strong);
    }

    &:-moz-meter-sub-optimum {
      --_fill: var(--loam-color-warning-strong);
    }

    &:-moz-meter-sub-sub-optimum {
      --_fill: var(--loam-color-danger-strong);
    }
  }

  /* Chromium and WebKit paint their own track box with a UA gradient once
     appearance is none; make it the same surface as the element. */
  :scope::-webkit-meter-bar {
    background: var(--loam-color-bg-subtle);
    block-size: 100%;
    border: none;
    border-radius: var(--loam-radius-full);
  }

  :scope::-webkit-meter-optimum-value {
    background: var(--_optimum);
    block-size: 100%;
    border-radius: var(--loam-radius-full);
  }

  :scope::-webkit-meter-suboptimum-value {
    background: var(--loam-color-warning-strong);
    block-size: 100%;
    border-radius: var(--loam-radius-full);
  }

  :scope::-webkit-meter-even-less-good-value {
    background: var(--loam-color-danger-strong);
    block-size: 100%;
    border-radius: var(--loam-radius-full);
  }

  :scope::-moz-meter-bar {
    background: var(--_fill);
    border-radius: var(--loam-radius-full);
  }

  /* Forced colors: track and fill are background-painted and would vanish.
     Every fill rule above is one pseudo-element deep, so these win by
     order, whichever band the value is in. */
  @media (forced-colors: active) {
    :scope {
      border: 1px solid CanvasText;
    }

    :scope::-webkit-meter-optimum-value {
      background: Highlight;
      forced-color-adjust: none;
    }

    :scope::-webkit-meter-suboptimum-value {
      background: Highlight;
      forced-color-adjust: none;
    }

    :scope::-webkit-meter-even-less-good-value {
      background: Highlight;
      forced-color-adjust: none;
    }

    :scope::-moz-meter-bar {
      background: Highlight;
      forced-color-adjust: none;
    }
  }
}
}

/* Modal */
@layer loamui.components {
@scope (.loam-Modal-popup) to ([class*="loam-"]) {
  :scope {
    /* Content decides the width: fit-content between a floor and a readable
       cap, never wider than the viewport. A confirmation shrink-wraps, a form
       grows. `--loam-modal-size` overrides all three bounds at once when a
       design needs an explicit width. */
    --_size: var(--loam-modal-size, fit-content);
    --_cap: var(--loam-modal-size, 32rem);
    --_floor: var(--loam-modal-size, 24rem);

    background: var(--loam-color-surface);
    border: 0;
    border-radius: var(--loam-radius-lg);
    box-shadow: var(--loam-shadow-lg);
    font-size: var(--loam-text-sm);
    inline-size: var(--_size);

    /* Set explicitly, not left to the UA default, so a host `* { margin: 0 }`
       reset can't stop the auto margins centring the dialog. */
    margin: auto;
    max-block-size: calc(100dvb - 2 * var(--loam-space-l));
    max-inline-size: min(100% - 2 * var(--loam-space-s), var(--_cap));
    min-inline-size: min(24rem, var(--_floor), 100% - 2 * var(--loam-space-s));
    overflow-block: auto;
    padding: var(--loam-space-s);

    &::backdrop {
      background: color-mix(in oklch, var(--loam-color-fg) 45%, transparent);
    }

    /* Shadows vanish in forced colors; a border keeps the dialog's edge. */
    @media (forced-colors: active) {
      border: 1px solid;
    }

    @media (prefers-reduced-motion: no-preference) {
      opacity: 1;
      scale: 1;
      transition:
        opacity var(--loam-duration-lg) var(--loam-ease),
        translate var(--loam-duration-lg) var(--loam-ease),
        scale var(--loam-duration-lg) var(--loam-ease),
        display var(--loam-duration-lg) var(--loam-ease) allow-discrete,
        overlay var(--loam-duration-lg) var(--loam-ease) allow-discrete;
      translate: 0 0;

      /* Exit is the transition BACK to the closed state — without these the
         dialog would sit fully opaque for the whole duration, then vanish
         (display/overlay flip discretely at the end). */
      &:not([open]) {
        opacity: 0;
        scale: 0.98;
        translate: 0 0.5rem;
      }

      &::backdrop {
        opacity: 1;
        transition:
          opacity var(--loam-duration-lg) var(--loam-ease),
          display var(--loam-duration-lg) var(--loam-ease) allow-discrete,
          overlay var(--loam-duration-lg) var(--loam-ease) allow-discrete;

        @starting-style {
          opacity: 0;
        }
      }

      &:not([open])::backdrop {
        opacity: 0;
      }

      @starting-style {
        opacity: 0;
        scale: 0.98;
        translate: 0 0.5rem;
      }
    }
  }

  h2.title {
    font-size: var(--loam-text-lg);
    font-weight: 600;
    margin-block: 0 var(--loam-space-2xs);
  }

  p.description {
    color: var(--loam-color-fg-muted);
    margin-block: 0 var(--loam-space-xs);
  }
}
}

/* Nav */
@layer loamui.components {
/* A nav landmark holding lists of links. It hosts consumer links and
   icons, hence the donut. A Group is the component's own details element,
   not core's Details (a boxed surface with a padded body, where a group in
   a nav is a line among lines), so the scope reaches into it and a list
   reads the same whether it sits in the nav or in a group. The lines
   themselves are set by the scope below this one. */
@scope (.loam-Nav) to ([class*="loam-"]) {
  :scope {
    display: block grid;
    gap: var(--loam-space-2xs);
  }

  /* A sidebar with several titled sections is one Root per section,
     stacked; the space between them is the nav's own, so the stack reads
     as one column without a wrapper rule. */
  .loam-Nav + :scope {
    margin-block-start: var(--loam-space-s);
  }

  /* A small label above the lists, set the same whatever element it
     renders as: a heading through `render` keeps the nav's own type, not
     the page's heading scale. */
  .title {
    color: var(--loam-color-fg-muted);
    font-family: var(--loam-font);
    font-size: var(--loam-text-xs);
    font-weight: 600;
    letter-spacing: 0.08em;
    line-height: 1.4;
    margin: 0;
    padding-inline: var(--loam-space-2xs);
    text-transform: uppercase;
  }

  ul {
    display: block grid;
    gap: var(--loam-space-fixed-2);
    list-style: none;
    margin: 0;
    padding: 0;

    /* A nested list indents a level, whether it sits in an item or in a
       group: a group's list is a nested list too, so the two indents are
       one value, the public --loam-nav-indent. */
    ul {
      padding-inline-start: var(--loam-nav-indent, var(--loam-space-xs));
    }
  }

  li {
    margin: 0;

    /* The item holding a dropdown is the panel's box where anchor
       positioning is missing and the panel sits under it, not in the top
       layer; harmless where the panel is anchored. */
    &:has(> .dropdown-trigger) {
      position: relative;
    }
  }
}

/* Every link and every group's summary is one line of the nav, set the
   same. Rooted at the line rather than the nav (as Table's sort control is
   rooted at itself) because a Link rendered as a button and handed to a
   Menu or Popover as its trigger sits inside that part's own wrapper,
   which the nav's donut fences out; rooted here, it is a line of the nav
   wherever it sits, and the donut fences only the consumer's parts inside
   a line. `.link` is the nav's word: a class of that name anywhere under
   the landmark is read as one of its lines. */
@scope (.loam-Nav :is(.link, summary.group-title)) to ([class*="loam-"]) {
  /* The marker's border, transparent at rest, so the current one does not
     shift its text; the size set here rather than inherited, so a group's
     links match the links beside it. No box of its own (a button brings
     the elements layer's; it is shed here), text set from the start, and
     the pointer for every element the line may render as. The line's
     height is the public --loam-nav-link-size; a consumer raises it to
     2.75rem where a thumb is the pointer, as in a mobile menu. */
  :scope {
    align-items: center;
    background: none;
    border: 0;
    border-radius: var(--loam-radius-md);
    box-shadow: none;
    color: var(--loam-color-fg);
    cursor: pointer;
    display: block flex;
    font-size: var(--loam-text-md);
    font-weight: 400;
    gap: var(--loam-space-2xs);
    line-height: 1.4;
    min-block-size: var(--loam-nav-link-size, 2.25rem);
    padding-block: var(--loam-space-3xs);
    padding-inline: var(--loam-space-2xs);
    text-align: start;
    text-decoration: none;
    white-space: normal;

    svg {
      block-size: auto;
      flex: none;
      inline-size: 1.25em;
    }

    /* The current destination is marked by a line and weight as well as a
       background, so it survives a flattened or absent background. The
       state is the ARIA attribute, whatever it says: page, location, step. */
    &[aria-current] {
      background: var(--loam-color-bg-subtle);
      border-color: var(--loam-color-primary-strong);
      color: var(--loam-color-fg-strong);
      font-weight: 600;
    }

    &:is(summary) {
      list-style: none;
      user-select: none;

      &::-webkit-details-marker {
        display: none;
      }
    }

    /* A line that opens something (a group's summary, a dropdown's
       trigger) ends in a chevron drawn from two borders, turning while
       the fold or panel is open. Borders, not a glyph, so forced colours
       keep it in the text colour. */
    &:is(summary, .dropdown-trigger)::after {
      block-size: 0.5em;
      border-block-end: 2px solid currentcolor;
      border-inline-end: 2px solid currentcolor;
      content: "";
      flex: none;
      inline-size: 0.5em;
      margin-block-start: -0.25em;
      margin-inline-start: auto;
      rotate: 45deg;

      @media (prefers-reduced-motion: no-preference) {
        transition: rotate var(--loam-duration-sm) var(--loam-ease);
      }
    }

    details.group[open] > &::after,
    &.dropdown-trigger[aria-expanded="true"]::after {
      margin-block-start: 0.25em;
      rotate: -135deg;
    }

    /* A group holding the current page is the reader's place too: its
       title takes the weight, and while the group is closed, the marker
       as well, so the location is never folded out of sight. */
    details.group:has(.link[aria-current]) > & {
      color: var(--loam-color-fg-strong);
      font-weight: 600;
    }

    details.group:not([open]):has(.link[aria-current]) > & {
      border-color: var(--loam-color-primary-strong);
    }

    /* The same for a dropdown: its panel is the trigger's next sibling,
       so a current link in there gives the trigger the weight, and the
       marker while the panel is closed. */
    &.dropdown-trigger:has(+ * .link[aria-current]) {
      color: var(--loam-color-fg-strong);
      font-weight: 600;
    }

    &.dropdown-trigger[aria-expanded="false"]:has(+ * .link[aria-current]) {
      border-color: var(--loam-color-primary-strong);
    }

    /* The marker's edge is the public --loam-nav-current-edge: the start
       edge in a column, block-end in a row, where the bar runs under the
       link. A style query is answered by ancestors, so it is declared on
       the Root or above, never on the link itself. */
    @container not style(--loam-nav-current-edge: block-end) {
      border-inline-start: 2px solid transparent;
    }

    @container style(--loam-nav-current-edge: block-end) {
      border-block-end: 2px solid transparent;
    }

    @media (hover: hover) {
      &:hover {
        background: var(--loam-color-bg-subtle);
        color: var(--loam-color-fg-strong);
      }
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        background var(--loam-duration-sm) var(--loam-ease),
        border-color var(--loam-duration-sm) var(--loam-ease),
        color var(--loam-duration-sm) var(--loam-ease);
    }

    /* Forced colours flatten the background and the marker's colour, so
       the current entry keeps its weight and the marker is named in the
       system highlight; the chevron already draws in the text colour.
       After the base rules: same specificity, order decides. */
    @media (forced-colors: active) {
      &[aria-current] {
        border-color: Highlight;
      }

      details.group:not([open]):has(.link[aria-current]) > & {
        border-color: Highlight;
      }

      &.dropdown-trigger[aria-expanded="false"]:has(+ * .link[aria-current]) {
        border-color: Highlight;
      }
    }
  }
}

/* A dropdown's panel: a native popover, so it leaves the nav for the top
   layer and carries its own scope; it hosts the consumer's list, hence the
   donut. It is a column of lines whatever edge the row around it draws
   the marker on, so the edge is reset here, where the links' style query
   will find it. */
@scope (.loam-Nav-dropdown) to ([class*="loam-"]) {
  :scope {
    /* The panel's width is the public --loam-nav-dropdown-size, capped to
       the viewport; a wide panel raises it and lays a grid inside. */
    --_size: var(--loam-nav-dropdown-size, 16rem);
    --loam-nav-current-edge: inline-start;

    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line);
    border-radius: var(--loam-radius-md);
    box-shadow: var(--loam-shadow-lg);
    inline-size: min(var(--_size), 90vi);
    margin: 0;
    padding: var(--loam-space-3xs);

    /* Enhanced: top layer + anchor positioning, tethered to the trigger by
       the per-instance anchor-name the two parts share through inline
       styles. Under the trigger's start edge; the browser flips it up or
       to the other side when it would leave the viewport. */
    @supports (anchor-name: --loam-probe) {
      /* The UA gives [popover] `inset: 0`; clear it so the area alone
         places the box. */
      inset: auto;
      margin-block: var(--loam-space-3xs);
      position: fixed;
      position-area: block-end span-inline-end;
      position-try-fallbacks: flip-block, flip-inline;

      @media (prefers-reduced-motion: no-preference) {
        opacity: 1;
        transition:
          opacity var(--loam-duration-md) var(--loam-ease),
          translate var(--loam-duration-md) var(--loam-ease),
          display var(--loam-duration-md) var(--loam-ease) allow-discrete,
          overlay var(--loam-duration-md) var(--loam-ease) allow-discrete;
        translate: 0 0;

        &:not(:popover-open) {
          opacity: 0;
          translate: 0 -0.25rem;
        }

        @starting-style {
          opacity: 0;
          translate: 0 -0.25rem;
        }
      }
    }

    /* Fallback: no anchor positioning (and no top layer), so the panel is
       an ordinary absolutely positioned box under its Item. */
    @supports not (anchor-name: --loam-probe) {
      inset-block-start: calc(100% + var(--loam-space-3xs));
      inset-inline-start: 0;
      position: absolute;
      z-index: var(--loam-z-popup);
    }

    /* Forced colours drop the shadow; the border, in the text colour,
       keeps the panel's edge. */
    @media (forced-colors: active) {
      border-color: CanvasText;
    }
  }

  /* The nav's own list rules stop at the panel (it is a loam- root), so a
     List inside it is set here the same way. */
  ul {
    display: block grid;
    gap: var(--loam-space-fixed-2);
    list-style: none;
    margin: 0;
    padding: 0;
  }

  li {
    margin: 0;
  }
}
}

/* Pagination */
@layer loamui.components {
@scope (.loam-Pagination) to ([class*="loam-"]) {
  :scope {
    /* The Button anatomy's height (incl. 1px borders), so the ellipsis cell
       stands in step with the links. */
    --_control-h: calc(1.2 * var(--loam-text-sm) + 2 * var(--loam-space-2xs) + 2px);
  }

  ul {
    align-items: center;
    display: block flex;
    flex-wrap: wrap;
    gap: var(--loam-space-3xs);
    list-style: none;
    margin: 0;
    padding: 0;
  }

  li {
    margin-block: 0;

    /* The current page is the ARIA state, detected rather than duplicated:
       the item declares the primary context and its Button answers it. */
    &:has(> [aria-current="page"]) {
      --loam-context: primary;
    }

    /* An unavailable direction keeps its space and drops out of reach. */
    &:has(> [data-disabled]) {
      opacity: var(--loam-disabled-opacity);
      pointer-events: none;
    }

    &.ellipsis {
      align-items: center;
      block-size: var(--_control-h);
      color: var(--loam-color-fg-dim);
      display: inline flex;
      justify-content: center;
      min-inline-size: var(--_control-h);
      user-select: none;
    }
  }
}
}

/* PasswordInput */
@layer loamui.components {
@scope (.loam-PasswordInput) to ([class*="loam-"]) {
  :scope {
    align-items: end;
    display: block grid;
    gap: var(--loam-space-2xs);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
  }
}
}

/* Popover */
@layer loamui.components {
@scope (.loam-Popover) {
  :scope {
    display: inline flow-root;
    position: relative;
  }
}

/* The popup escapes the wrapper into the top layer when enhanced, so it
   carries its own scope. */
@scope (.loam-Popover-popup) to ([class*="loam-"]) {
  :scope {
    /* Content decides the width between a floor and a readable cap; the
       cap is the public `--loam-popover-size`, the floor derives from it. */
    --_size: var(--loam-popover-size, 20rem);

    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line);
    border-radius: var(--loam-radius-md);
    box-shadow: var(--loam-shadow-lg);
    inline-size: max-content;
    margin: 0;
    max-inline-size: min(var(--_size), 90vi);
    min-inline-size: calc(var(--_size) * 0.6);
    padding: var(--loam-space-xs);

    /* Enhanced: top layer + anchor positioning, tethered to the trigger.
       position-anchor / anchor-name are set from inline styles with a
       per-instance name shared by Trigger and Popup. */
    @supports (anchor-name: --loam-probe) {
      /* The UA gives [popover] `inset: 0`; clear it so only the insets each
         branch sets take part, otherwise the box is over-constrained. */
      inset: auto;
      inset-inline-start: anchor(start);
      position: fixed;
      position-try: flip-block;

      &[data-side="bottom"] {
        inset-block-start: calc(anchor(end) + var(--loam-space-3xs));
      }

      &[data-side="top"] {
        inset-block-end: calc(anchor(start) + var(--loam-space-3xs));
      }

      @media (prefers-reduced-motion: no-preference) {
        opacity: 1;
        transition:
          opacity var(--loam-duration-lg) var(--loam-ease),
          translate var(--loam-duration-lg) var(--loam-ease),
          display var(--loam-duration-lg) var(--loam-ease) allow-discrete,
          overlay var(--loam-duration-lg) var(--loam-ease) allow-discrete;
        translate: 0 0;

        /* Exit: transition back to the closed state (see Modal). */
        &:not(:popover-open) {
          opacity: 0;
          translate: 0 -0.25rem;
        }

        &[data-side="top"] {
          &:not(:popover-open) {
            translate: 0 0.25rem;
          }

          @starting-style {
            translate: 0 0.25rem;
          }
        }

        @starting-style {
          opacity: 0;
          translate: 0 -0.25rem;
        }
      }
    }

    /* Fallback: no anchor positioning (and no top layer) — anchor to the
       wrapper as a normal absolutely-positioned panel. */
    @supports not (anchor-name: --loam-probe) {
      inset-inline-start: 0;
      position: absolute;
      z-index: var(--loam-z-popup);

      &[data-side="bottom"] {
        inset-block-start: calc(100% + var(--loam-space-3xs));
      }

      &[data-side="top"] {
        inset-block-end: calc(100% + var(--loam-space-3xs));
      }
    }
  }

  h2.title {
    font-size: var(--loam-text-md);
    font-weight: 600;
    margin: 0;
  }

  p.description {
    color: var(--loam-color-fg-muted);
    margin-block-end: 0;
    margin-inline: 0;

    &:not(h2.title + *) {
      margin-block-start: 0;
    }

    h2.title + & {
      margin-block-start: var(--loam-space-3xs);
    }
  }
}
}

/* Price */
@layer loamui.components {
/*
 * A Price sizes nothing: the amount takes the font of whatever it sits in,
 * so the same element reads as a headline in a plan and as a figure in a
 * table cell. The figures themselves are the `data` element rule in
 * elements.css; this dresses only the qualifier.
 */
@scope (.loam-Price) to ([class*="loam-"]) {
  /* The qualifier is secondary: muted, small, and back to the running
     figures so it never competes with the amount. */
  small.per {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);
    font-variant-numeric: normal;
    font-weight: 400;
    margin-inline-start: 0.35em;
  }
}
}

/* Progress */
@layer loamui.components {
/* The root lays out the label and the bar; the bar is the native
   <progress>. `appearance: none` opts out of the UA painting, so the
   element is the track and the vendor pseudo-elements carry the fill —
   each browser names them differently, so each gets its own rule. */
@scope (.loam-Progress) to ([class*="loam-"]) {
  :scope {
    --_h: 0.625rem;

    display: block grid;
    gap: var(--loam-space-3xs);
    inline-size: 100%;

    &[data-size="sm"] {
      --_h: 0.375rem;
    }

    &[data-size="lg"] {
      --_h: 1rem;
    }
  }

  span.label {
    font-size: var(--loam-text-sm);
  }

  progress {
    appearance: none;
    background: var(--loam-color-bg-subtle);
    block-size: var(--_h);
    border: none;
    border-radius: var(--_radius, var(--loam-radius-full));
    display: block flow;
    inline-size: 100%;
    overflow: clip;
  }

  /* Chromium and WebKit paint their own track box once appearance is none;
     make it the same surface as the element. */
  progress::-webkit-progress-bar {
    background: var(--loam-color-bg-subtle);
    border-radius: inherit;
  }

  /* The fill IS the primary token — a `--loam-context` region re-answers
     it (see the Contexts section of tokens.css), so status needs no
     component code here. */
  progress::-webkit-progress-value {
    background: var(--loam-color-primary-strong);
    border-radius: inherit;

    @media (prefers-reduced-motion: no-preference) {
      transition: inline-size var(--loam-duration-lg) var(--loam-ease);
    }
  }

  progress::-moz-progress-bar {
    background: var(--loam-color-primary-strong);
    border-radius: inherit;
  }

  /* No value: a band of the fill colour on the track. It sweeps where
     motion is welcome; otherwise it stands still, and the label says the
     wait is under way. Firefox paints its own indeterminate bar, which is
     hidden so only the track's band shows. */
  progress:indeterminate {
    background-image: linear-gradient(
      90deg,
      transparent 0%,
      var(--loam-color-primary-strong) 50%,
      transparent 100%
    );
    background-repeat: no-repeat;
    background-size: 40% 100%;

    @media (prefers-reduced-motion: no-preference) {
      animation: loam-progress-sweep calc(var(--loam-duration-lg) * 5) var(--loam-ease) infinite;
    }
  }

  progress:indeterminate::-webkit-progress-bar {
    background: transparent;
  }

  progress:indeterminate::-moz-progress-bar {
    background: transparent;
  }

  /* Stripes: on-strong at low alpha over the fill, so they read in every
     context. */
  :scope[data-striped] progress::-webkit-progress-value {
    background-image: repeating-linear-gradient(
      45deg,
      color-mix(in oklch, var(--loam-color-on-strong) 22%, transparent) 0,
      color-mix(in oklch, var(--loam-color-on-strong) 22%, transparent) 0.5rem,
      transparent 0.5rem,
      transparent 1rem
    );
    background-size: 1.414rem 1.414rem;
  }

  :scope[data-striped] progress::-moz-progress-bar {
    background-image: repeating-linear-gradient(
      45deg,
      color-mix(in oklch, var(--loam-color-on-strong) 22%, transparent) 0,
      color-mix(in oklch, var(--loam-color-on-strong) 22%, transparent) 0.5rem,
      transparent 0.5rem,
      transparent 1rem
    );
    background-size: 1.414rem 1.414rem;
  }

  @media (prefers-reduced-motion: no-preference) {
    :scope[data-animated] progress::-webkit-progress-value {
      animation: loam-progress-stripes calc(var(--loam-duration-lg) * 3) linear infinite;
    }

    :scope[data-animated] progress::-moz-progress-bar {
      animation: loam-progress-stripes calc(var(--loam-duration-lg) * 3) linear infinite;
    }
  }

  /* Forced colors: track and fill are background-painted and would vanish.
     Every fill rule above is one pseudo-element deep, so these win by order. */
  @media (forced-colors: active) {
    progress {
      border: 1px solid CanvasText;
    }

    progress::-webkit-progress-value {
      background: Highlight;
      forced-color-adjust: none;
    }

    progress::-moz-progress-bar {
      background: Highlight;
      forced-color-adjust: none;
    }

    progress:indeterminate {
      background-image: linear-gradient(90deg, transparent 0%, Highlight 50%, transparent 100%);
      forced-color-adjust: none;
    }
  }
}

@keyframes loam-progress-stripes {
  from {
    background-position: 0 0;
  }

  to {
    background-position: 1.414rem 0;
  }
}

@keyframes loam-progress-sweep {
  from {
    background-position: -40% 0;
  }

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

/* QuantityInput */
@layer loamui.components {
/*
 * QuantityInput — a bare <input type="number"> between two Buttons.
 *
 * The input keeps the field look elements.css gives every textual input
 * (border, radius, padding, surface, the focus ring) and the buttons are
 * core Buttons, so all three share the derived control height and align
 * by construction. This file only centres the figure, sizes the box to a
 * few digits and hides the native spinner, whose job the buttons now do.
 *
 * Forced colours: nothing to add. The input's border and the Buttons'
 * borders survive in system colours, disabled buttons take GrayText, and
 * the invalid state below is a border colour plus aria-invalid, which the
 * Field's error text carries in words.
 */
@scope (.loam-QuantityInput) to ([class*="loam-"]) {
  :scope {
    align-items: center;
    display: block flex;
    gap: var(--loam-space-3xs);

    /* Shrink-wrapped even as a stretched Field child: a count is a small
       thing, and a wide box would read as a bigger question. */
    inline-size: fit-content;
  }

  input {
    /* The native spinner would duplicate the buttons in a corner of the
       box. `appearance: textfield` drops it in Firefox; WebKit needs the
       pseudo-element told directly (below). */
    appearance: textfield;

    /* Content-box so the character widths below mean characters, not
       characters minus padding. Three digits covers any count a person
       adjusts one at a time; larger numbers are typed into an Input. */
    box-sizing: content-box;
    font-variant-numeric: lining-nums tabular-nums;
    inline-size: 3ch;
    text-align: center;

    &::-webkit-inner-spin-button,
    &::-webkit-outer-spin-button {
      appearance: none;
      margin: 0;
    }

    &[aria-invalid="true"] {
      --loam-color-ring: var(--loam-color-danger-strong);

      border-color: var(--loam-color-danger);
    }

    &:disabled {
      background: var(--loam-color-bg-subtle);
      opacity: var(--loam-disabled-opacity);
    }
  }

  /* Where the platform sizes a field to its content, let it: the floor
     above still applies, and a four-digit value no longer clips. */
  @supports (field-sizing: content) {
    input {
      field-sizing: content;
      inline-size: auto;
      min-inline-size: 3ch;
    }
  }
}
}

/* Radio */
@layer loamui.components {
/* The control is a native radio — the elements layer paints it with
   accent-color (the neutral primary's audited -strong, so a --loam-context
   region keeps 3:1 under the dot). The component adds size, the invalid
   affordance, the label row and the group layout. */
@scope (.loam-Radio) to ([class*="loam-"]) {
  :scope {
    accent-color: var(--loam-color-primary-strong);
    block-size: 1.15em;
    flex-shrink: 0;
    inline-size: 1.15em;
    margin: 0;
  }

  /* ARIA carries the invalid state on the group, not the radios, so a
     radio answers its group's aria-invalid rather than its own. The danger
     ring is a box-shadow so it stacks with rather than replaces the
     elements-layer :focus-visible ring; the focus ring itself turns
     danger-coloured. Mirrors Switch. */
  .loam-RadioGroup[aria-invalid="true"] :scope {
    --loam-color-ring: var(--loam-color-danger-strong);

    box-shadow: 0 0 0 2px var(--loam-color-danger);
  }

  /* Forced colors drop box-shadow, so the invalid affordance moves to an
     outline in a system colour; focus keeps its own, further-out ring. */
  @media (forced-colors: active) {
    .loam-RadioGroup[aria-invalid="true"] :scope {
      outline: 2px solid LinkText;
      outline-offset: 1px;

      &:focus-visible {
        outline: var(--loam-ring-width) solid Highlight;
        outline-offset: 2px;
      }
    }
  }
}

@scope (.loam-Radio-wrapper) to ([class*="loam-"]) {
  :scope {
    align-items: flex-start;
    cursor: pointer;
    display: inline flex;
    gap: var(--loam-space-2xs);

    &:has(input:disabled) {
      cursor: not-allowed;
      opacity: var(--loam-disabled-opacity);
    }
  }

  span.body {
    display: block flex;
    flex-direction: column;
    gap: 0.125em;
  }

  span.label {
    font-size: var(--loam-text-sm);
    font-weight: 500;
  }

  span.description {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-xs);
  }
}

/* ---- Group ---- a Fieldset (reset and legend come from that layer) whose
   direct children are the legend, the description, the error and the
   Radios. The fieldset's column is the vertical layout; horizontal turns
   it into a wrapping row and gives the text parts a row of their own. */
@scope (.loam-RadioGroup) to ([class*="loam-"]) {
  :scope[data-orientation="horizontal"] {
    column-gap: var(--loam-space-s);
    flex-flow: row wrap;
  }

  p.description {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);
    margin: 0;
  }

  p.error {
    color: var(--loam-color-danger);
    font-size: var(--loam-text-sm);
    margin: 0;
  }

  :scope[data-orientation="horizontal"] > :is(p.description, p.error) {
    flex-basis: 100%;
  }
}
}

/* Range */
@layer loamui.components {
/* The bare control is the `<input type="range">` itself — no wrapper element
   in that form — so the input is its own scope root. */
@scope (.loam-Range) to ([class*="loam-"]) {
  :scope {
    --_track-h: 0.4167em;

    /* The thumb's diameter is the public --loam-range-thumb-size. Its
       default is written on the token, not in em, because the marks and
       the output below read the same property from a smaller font size
       and must land on the same pixel. */
    --_thumb: var(--loam-range-thumb-size, calc(1.25 * var(--loam-text-sm)));

    /* appearance: none opts out of native painting, so the track and thumb
       below carry the colour. -strong (not raw primary) so a --loam-context
       region's fill stays 3:1 against the on-strong thumb border. */
    appearance: none;
    background: transparent;
    cursor: pointer;
    font-size: var(--loam-text-sm);
    inline-size: 100%;
    margin: 0;

    &[aria-invalid="true"] {
      --loam-color-ring: var(--loam-color-danger-strong);
    }

    &:disabled {
      cursor: not-allowed;
      opacity: var(--loam-disabled-opacity);
    }
  }

  :scope::-webkit-slider-runnable-track {
    background: var(--loam-color-line-strong);
    block-size: var(--_track-h);
    border-radius: var(--loam-radius-full);
  }

  :scope::-moz-range-track {
    background: var(--loam-color-line-strong);
    block-size: var(--_track-h);
    border-radius: var(--loam-radius-full);
  }

  /* Firefox paints the filled portion explicitly. */
  :scope::-moz-range-progress {
    background: var(--loam-color-primary-strong);
    block-size: var(--_track-h);
    border-radius: var(--loam-radius-full);
  }

  :scope::-webkit-slider-thumb {
    appearance: none;
    background: var(--loam-color-primary-strong);
    block-size: var(--_thumb);
    border: 2px solid var(--loam-color-on-strong);
    border-radius: var(--loam-radius-full);
    box-shadow: var(--loam-shadow-sm);
    inline-size: var(--_thumb);
    margin-block-start: calc((var(--_track-h) - var(--_thumb)) / 2);

    /* Identity at rest — the hover grow transitions both ways. */
    scale: 1;

    @media (prefers-reduced-motion: no-preference) {
      transition:
        scale var(--loam-duration-md) var(--loam-ease),
        box-shadow var(--loam-duration-md) var(--loam-ease);
    }
  }

  :scope::-moz-range-thumb {
    background: var(--loam-color-primary-strong);
    block-size: var(--_thumb);
    border: 2px solid var(--loam-color-on-strong);
    border-radius: var(--loam-radius-full);
    box-shadow: var(--loam-shadow-sm);
    inline-size: var(--_thumb);
    scale: 1;

    @media (prefers-reduced-motion: no-preference) {
      transition:
        scale var(--loam-duration-md) var(--loam-ease),
        box-shadow var(--loam-duration-md) var(--loam-ease);
    }
  }

  /* Invalid is carried on the thumb, the one part every browser paints. */
  :scope[aria-invalid="true"]::-webkit-slider-thumb {
    background: var(--loam-color-danger-strong);
  }

  :scope[aria-invalid="true"]::-moz-range-thumb {
    background: var(--loam-color-danger-strong);
  }

  /* The ring belongs on the thumb (pseudo-element, box-shadow — outline is
     unreliable on ::-webkit-slider-thumb); suppress the elements ring. */
  :scope:focus-visible {
    outline: none;
  }

  :scope:focus-visible::-webkit-slider-thumb {
    box-shadow: 0 0 0 var(--loam-ring-width) var(--loam-color-ring);
  }

  :scope:focus-visible::-moz-range-thumb {
    box-shadow: 0 0 0 var(--loam-ring-width) var(--loam-color-ring);
  }

  @media (hover: hover) {
    :scope:hover:not(:disabled)::-webkit-slider-thumb {
      scale: 1.08;
    }

    :scope:hover:not(:disabled)::-moz-range-thumb {
      scale: 1.08;
    }
  }

  /* Forced colors: the custom track and thumb are background-painted and
     would vanish (appearance: none opts out of native painting). */
  @media (forced-colors: active) {
    /* The thumb's box-shadow ring is dropped here, so focus and invalid come
       back as outlines on the input itself. */
    :scope:focus-visible {
      outline: var(--loam-ring-width) solid Highlight;
      outline-offset: 2px;
    }

    :scope[aria-invalid="true"] {
      outline: 2px solid LinkText;
      outline-offset: 1px;
    }

    :scope::-webkit-slider-runnable-track {
      background: ButtonText;
      forced-color-adjust: none;
    }

    :scope::-moz-range-track {
      background: ButtonText;
      forced-color-adjust: none;
    }

    :scope::-moz-range-progress {
      background: Highlight;
      forced-color-adjust: none;
    }

    :scope::-webkit-slider-thumb {
      background: Highlight;
      border-color: Canvas;
      forced-color-adjust: none;
    }

    :scope::-moz-range-thumb {
      background: Highlight;
      border-color: Canvas;
      forced-color-adjust: none;
    }
  }
}

/* The optional wrapper: a stacked grid carrying `--_value` (the thumb's
   position as a fraction, set by the component) for the output above. */
@scope (.loam-Range-root) to ([class*="loam-"]) {
  :scope {
    display: block grid;
    gap: var(--loam-space-3xs);
    inline-size: 100%;
  }
}

/* Ticks and labels under the track, read from the same
   --loam-range-thumb-size as the input: the thumb's centre travels from
   half a thumb in to half a thumb from the end, so a mark at fraction
   `--_at` sits at thumb/2 + (width - thumb) * --_at. The ticks are
   borders, so forced colours keep them in CanvasText without a rule of
   their own. */
@scope (.loam-Range-marks) to ([class*="loam-"]) {
  :scope {
    --_thumb: var(--loam-range-thumb-size, calc(1.25 * var(--loam-text-sm)));

    block-size: calc(0.5em + 1lh);
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-xs);
    position: relative;
  }

  /* Centred on its tick: the shift is toward the line's start, so it
     reverses with the writing direction. */
  span {
    inset-block-start: 0;
    inset-inline-start: calc(var(--_thumb) / 2 + (100% - var(--_thumb)) * var(--_at, 0));
    padding-block-start: 0.5em;
    position: absolute;
    translate: -50%;
    white-space: nowrap;

    &:dir(rtl) {
      translate: 50%;
    }

    &::before {
      block-size: 0.5em;
      border-inline-start: 1px solid var(--loam-color-line-strong);
      content: "";
      inset-block-start: 0;
      inset-inline-start: 50%;
      position: absolute;
    }
  }
}

/* The value bubble, on the row above the input in the root's grid; the
   same thumb geometry as the marks places it over the thumb, and the
   centring shift reverses with the writing direction. */
@scope (.loam-Range-output) {
  :scope {
    --_thumb: var(--loam-range-thumb-size, calc(1.25 * var(--loam-text-sm)));

    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line-strong);
    border-radius: var(--loam-radius-sm);
    font-size: var(--loam-text-xs);
    font-variant-numeric: lining-nums tabular-nums;
    inline-size: fit-content;
    justify-self: start;
    margin-inline-start: calc(var(--_thumb) / 2 + (100% - var(--_thumb)) * var(--_value, 0));
    order: -1;
    padding-block: var(--loam-space-3xs);
    padding-inline: var(--loam-space-2xs);
    translate: -50%;
    white-space: nowrap;

    &:dir(rtl) {
      translate: 50%;
    }
  }
}
}

/* Rating */
@layer loamui.components {
/*
 * Rating — stars that are real inputs. In input mode the root is a Fieldset
 * (its reset and legend come from that layer) holding one native radio per
 * star, hidden under its glyph; in display mode the same glyphs are one
 * named picture. The label always names the stars: unshown, the
 * component gives it .loam-VisuallyHidden. The glyph is an inline svg
 * painted with currentcolor and sized in em, so it follows the
 * surrounding type. Each star carries two
 * private numbers: --_paint (its colour) and --_amount (how much of the
 * fill shows, 0–1); everything else derives from those.
 */
@scope (.loam-Rating) to ([class*="loam-"]) {
  :scope {
    /* Accent is the library's chromatic flourish, so a --loam-context region
       remaps the filled colour through the token layer. */
    --_empty: var(--loam-color-line-strong);
    --_filled: var(--loam-color-accent);

    /* Display mode sits inline: label, then the picture. (Input mode is a
       Fieldset, which stacks legend over stars.) */
    &[data-read-only] {
      align-items: center;
      display: inline flex;
      gap: var(--loam-space-2xs);
    }

    &:disabled {
      opacity: var(--loam-disabled-opacity);
    }
  }

  span.label {
    font-size: var(--loam-text-sm);
    font-weight: 500;
  }

  span.stars {
    display: inline flex;
  }

  .star {
    --_amount: 0;
    --_paint: var(--_empty);

    display: inline flex;
    padding-inline: 0.125em;
    position: relative;
  }

  /* The native radio is invisible but present: it covers its glyph so the
     glyph is the hit area (at least 24px, WCAG 2.5.8), stays focusable and
     keeps the group's arrow keys. */
  input {
    block-size: max(100%, 24px);
    inline-size: max(100%, 24px);
    inset-block-start: 50%;
    inset-inline-start: 50%;
    margin: 0;
    opacity: 0;
    position: absolute;
    translate: -50% -50%;

    &:not(:disabled) {
      cursor: pointer;
    }

    /* The ring belongs on the glyph, not on the invisible input. */
    &:focus-visible {
      outline: none;
    }
  }

  .star:has(input:focus-visible) {
    border-radius: var(--loam-radius-sm);
    outline: var(--loam-ring-width) solid var(--loam-color-ring);
    outline-offset: 2px;
  }

  svg {
    block-size: 1.25em;
    color: var(--_paint);
    inline-size: 1.25em;

    @media (prefers-reduced-motion: no-preference) {
      transition: color var(--loam-duration-sm) var(--loam-ease);
    }
  }

  path.outline {
    fill: none;
    stroke: currentcolor;
    stroke-linejoin: round;
    stroke-width: 1.5px;
  }

  /* The fill is clipped to the star's amount from the reading edge; the
     glyph is mirrored in RTL so the clipped side is still the far one. */
  path.fill {
    clip-path: inset(0 calc((1 - var(--_amount)) * 100%) 0 0);
    fill: currentcolor;
  }

  svg:dir(rtl) {
    scale: -1 1;
  }

  /* ---- Input mode: a star is filled when it, or a later star, is checked. */
  .star:is(:has(input:checked), :has(~ .star input:checked)) {
    --_amount: 1;
    --_paint: var(--_filled);
  }

  /* ---- Display mode: the amount comes from the value, rounded to halves. */
  .star[data-fill="half"] {
    --_amount: 0.5;
    --_paint: var(--_filled);
  }

  .star[data-fill="full"] {
    --_amount: 1;
    --_paint: var(--_filled);
  }

  /* Hover previews the choice: the stars up to the pointer fill and the
     rest empty, whatever is checked. */
  @media (hover: hover) {
    span.stars:has(input:hover:not(:disabled)) .star {
      --_amount: 0;
      --_paint: var(--_empty);

      &:is(:has(input:hover), :has(~ .star input:hover)) {
        --_amount: 1;
        --_paint: var(--_filled);
      }
    }
  }

  /* Forced colours would flatten fill and stroke to one colour and lose
     which stars are filled: keep the glyph's own paint and answer it in
     system colours — Highlight for filled, CanvasText outline for empty. */
  @media (forced-colors: active) {
    :scope {
      --_empty: CanvasText;
      --_filled: Highlight;
    }

    svg {
      forced-color-adjust: none;
    }
  }
}
}

/* Search */
@layer loamui.components {
/*
 * Search — a <search> landmark around a native form. The scope arranges
 * the form's row and hides the label; the box and the button are the
 * library's own Input and Button, behind the donut, and keep their own
 * declarations. Nothing here is painted, so there is no forced-colours
 * treatment to write: the box and the button carry their own.
 */
@scope (.loam-Search) to ([class*="loam-"]) {
  /* <search> is block-level in every browser that knows it; declared so
     the row also holds where the element is still unknown. */
  :scope {
    display: block flow;
  }

  /* One row: the first cell grows, the button keeps its width, and both sit
     on the same baseline when a Field's label and hint sit above the box.
     A grid, not flex, because the first cell is a core Input or a core
     Field (past the donut): the track sizes it without styling it. */
  form {
    align-items: end;
    display: block grid;
    gap: var(--loam-space-2xs);
    grid-template-columns: minmax(0, 1fr) auto;
  }

  /* type="search" keeps the platform's clear affordance: neither Input.css
     nor elements.css sets appearance on the control, so there is nothing
     to restore here. */
}
}

/* SegmentedControl */
@layer loamui.components {
/* A native fieldset of radios dressed as a pill. The browser's fieldset
   border and padding are reset here rather than by core's Fieldset: the
   row is the whole anatomy. Each segment is a label around a radio the
   component hides with .loam-VisuallyHidden, so the label is the target
   and the input keeps focus and the arrow keys. The pill fits the control
   height contract: 1px borders, the inset and the segment's own padding
   add up to Button's padding-block on either side of a 1.2 line. Donut:
   the segments host consumer labels and icons. */
@scope (.loam-SegmentedControl) to ([class*="loam-"]) {
  :scope {
    --_inset: 2px;

    align-items: center;
    background: var(--loam-color-bg-subtle);
    border: 1px solid var(--loam-color-line);
    border-radius: var(--loam-radius-full);
    display: inline flex;
    gap: var(--_inset);
    margin: 0;
    min-inline-size: 0;
    padding: var(--_inset);
  }

  /* The legend leaves the fieldset's legend slot and lines up before the
     segments as the text of an anonymous flex item. display: contents, not
     a float: the spec makes a floated legend an ordinary child, but WebKit
     still draws it in the slot, across the pill's border. A box-less legend
     takes no padding, so the room around it is the pill's and the first
     segment's. Hidden, it wears loam-VisuallyHidden and is out of this
     scope. */
  legend {
    color: var(--loam-color-fg-muted);
    display: contents;
    font-size: var(--loam-text-sm);
    font-weight: 500;
    line-height: 1.2;
  }

  :scope:has(> legend:not([class~="loam-VisuallyHidden"])) {
    padding-inline-start: calc(var(--_inset) + var(--loam-space-2xs));
  }

  label.segment {
    align-items: center;
    border: 1px solid transparent;
    border-radius: var(--loam-radius-full);
    color: var(--loam-color-fg-muted);
    cursor: pointer;
    display: inline flex;
    font-size: var(--loam-text-sm);
    font-weight: 500;
    gap: var(--loam-space-2xs);
    justify-content: center;
    line-height: 1.2;

    /* The segment's own height (the line and Button's padding, less the
       inset the padding gives up on each side; the borders cancel), so an
       icon-only segment is a circle and a text one grows from it. */
    min-inline-size: var(
      --loam-segmented-control-segment-size,
      calc(1.2 * var(--loam-text-sm) + 2 * var(--loam-space-2xs) - 2 * var(--_inset))
    );
    padding-block: calc(var(--loam-space-2xs) - var(--_inset) - 1px);
    padding-inline: var(--loam-space-xs);
    user-select: none;
    white-space: nowrap;

    legend:not([class~="loam-VisuallyHidden"]) + & {
      margin-inline-start: var(--loam-space-3xs);
    }

    /* The consumer's icon, sized on the text. An icon-only segment is the
       icon beside its hidden text (the name a bare icon lacks), and sits in
       a circle. Detected from that pair, never from the svg being the last
       element: bare text after an icon is not an element, so `svg:last-child`
       would strip the padding from "icon + word" too. The hidden text's
       class is written as an attribute: the donut gate reads a `.loam-`
       anywhere in a subject as dead, and this one is a :has() argument. */
    svg {
      block-size: auto;
      flex: none;
      inline-size: 1em;
    }

    &:has(> input:first-child + svg + [class~="loam-VisuallyHidden"]:last-child) {
      padding-inline: 0;
    }

    /* The chosen segment lifts onto a surface. The line-strong edge is
       what carries the state at 3:1; surface on the subtle pill alone
       would not. */
    &:has(input:checked) {
      background: var(--loam-color-surface);
      border-color: var(--loam-color-line-strong);
      box-shadow: var(--loam-shadow-sm);
      color: var(--loam-color-fg-strong);
    }

    /* The ring belongs on the segment; the input's own ring is clipped
       away with it. */
    &:has(input:focus-visible) {
      outline: var(--loam-ring-width) solid var(--loam-color-ring);
      outline-offset: 2px;
    }

    &:has(input:disabled) {
      cursor: not-allowed;
      opacity: var(--loam-disabled-opacity);
    }

    @media (hover: hover) {
      &:hover:not(:has(input:checked, input:disabled)) {
        color: var(--loam-color-fg);
      }
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        background var(--loam-duration-sm) var(--loam-ease),
        border-color var(--loam-duration-sm) var(--loam-ease),
        box-shadow var(--loam-duration-md) var(--loam-ease),
        color var(--loam-duration-sm) var(--loam-ease);
    }
  }

  /* Forced colours strip the paint that says "chosen", so the state moves
     to the system highlight; the ring keeps the system focus colour, and
     a disabled segment, whose opacity is flattened, is named in GrayText.
     After the base rules: same specificity, order decides. */
  @media (forced-colors: active) {
    label.segment:has(input:checked) {
      background: Highlight;
      border-color: Highlight;
      color: HighlightText;
      forced-color-adjust: none;
    }

    label.segment:has(input:focus-visible) {
      outline-color: Highlight;
    }

    label.segment:has(input:disabled) {
      color: GrayText;
    }
  }
}
}

/* Select */
@layer loamui.components {
/* The select keeps the field look elements.css gives every textual
   control (surface, border, radius, padding, the focus ring); this scope
   adds the states and the chevron. A select cannot host a pseudo-element,
   and a mask-image would mask the control itself, text included, so the
   chevron is a sibling svg and the box exists to position it and for
   nothing else. */
@scope (.loam-Select-field) to ([class*="loam-"]) {
  :scope {
    display: block flex;

    /* The select's own size (elements.css), restated so the em-sized
       chevron follows the control's type, not the page's. */
    font-size: var(--loam-text-md);
    position: relative;
  }

  select {
    /* The native arrow would sit under the chevron. */
    appearance: none;
    cursor: pointer;
    flex: 1;
    min-inline-size: 0;

    /* Room for the chevron on the trailing side: its width plus the
       inset it sits at plus the gap it keeps from the text. */
    padding-inline-end: calc(1.125em + var(--loam-space-xs) * 2);

    &:focus-visible {
      border-color: var(--loam-color-primary);
    }

    &:disabled {
      background: var(--loam-color-bg-subtle);
      cursor: not-allowed;
      opacity: var(--loam-disabled-opacity);
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        border-color var(--loam-duration-sm) var(--loam-ease),
        box-shadow var(--loam-duration-md) var(--loam-ease);
    }
  }

  /* Autofilled: the border carries the signal and an inset shadow wider
     than the control paints the tint (background-color cannot be
     overridden on an autofilled field). :autofill and :-webkit-autofill
     must stay in separate blocks — one unsupported selector invalidates a
     shared list. Declared before the error state so danger wins at equal
     specificity. */
  select:autofill {
    border-color: color-mix(in oklab, var(--loam-color-primary) 50%, var(--loam-color-line-strong));
    box-shadow: inset 0 0 0 100px
      color-mix(in oklab, var(--loam-color-primary) 8%, var(--loam-color-surface));
  }

  select:-webkit-autofill {
    border-color: color-mix(in oklab, var(--loam-color-primary) 50%, var(--loam-color-line-strong));
    box-shadow: inset 0 0 0 100px
      color-mix(in oklab, var(--loam-color-primary) 8%, var(--loam-color-surface));
  }

  /* Invalid after a composed Field.Error or a native submit attempt. */
  select[aria-invalid="true"] {
    --loam-color-ring: var(--loam-color-danger-strong);

    border-color: var(--loam-color-danger);
  }

  /* The unanswered prompt stays muted but AA-readable: it is the
     control's visible value until answered, i.e. instruction text. */
  select:has(option[value=""]:checked) {
    color: var(--loam-color-fg-muted);
  }

  svg.chevron {
    block-size: 1.125em;
    color: var(--loam-color-fg-muted);
    flex-shrink: 0;

    /* Fluid: overrides the SVG's 16px width/height attributes. */
    inline-size: 1.125em;
    inset-block-start: 50%;
    inset-inline-end: var(--loam-space-xs);
    pointer-events: none;
    position: absolute;
    translate: 0 -50%;
  }

  /* Forced colours drop the danger border colour, so the invalid state
     moves to an outline in a system colour; the focus ring is offset
     further out and still reads as its own ring. Mirrors Input. */
  @media (forced-colors: active) {
    select[aria-invalid="true"] {
      outline: 2px solid LinkText;
      outline-offset: 1px;

      &:focus-visible {
        outline: var(--loam-ring-width) solid Highlight;
        outline-offset: 2px;
      }
    }
  }
}
}

/* Separator */
@layer loamui.components {
@scope (.loam-Separator) {
  :scope {
    align-self: stretch;
    border: 0;
    border-block-start: 1px solid var(--loam-color-line);
    margin: 0;

    &[data-orientation="vertical"] {
      block-size: auto;
      border-block-start: 0;
      border-inline-start: 1px solid var(--loam-color-line);
      inline-size: 0;
      min-block-size: 1em;
    }
  }
}
}

/* SignpostLink */
@layer loamui.components {
/* The signpost inherits its font size, so it reads as the prose, heading or
   card it sits in and the arrow (in em) rescales with it. */
@scope (.loam-SignpostLink) to ([class*="loam-"]) {
  :scope {
    align-items: center;
    border-radius: var(--loam-radius-sm);
    color: var(--loam-color-fg-strong);
    display: inline flex;
    font-weight: 600;
    gap: var(--loam-space-2xs);
    max-inline-size: fit-content;
    text-decoration: none;
  }

  /* The underline is always drawn with a transparent colour, so colour,
     thickness and offset can each transition. */
  span.label {
    text-decoration: underline;
    text-decoration-color: transparent;
    text-decoration-thickness: max(0.08em, 1px);
    text-underline-offset: 0.15em;

    :scope:active & {
      text-decoration-color: var(--loam-color-primary-strong);
      text-decoration-thickness: max(0.16em, 2px);
      text-underline-offset: 0.15em;
      transition: none;
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        text-decoration-color var(--loam-duration-md) var(--loam-ease),
        text-decoration-thickness var(--loam-duration-md) var(--loam-ease),
        text-underline-offset var(--loam-duration-md) var(--loam-ease-elastic);
    }
  }

  /* The circled arrow rides the font size, so the signpost rescales as one
     piece wherever a consumer changes it. Solid fill on the -strong family
     with on-strong — the audited pairing — and a context region recolours
     it by re-answering the token. */
  span.icon {
    background: var(--loam-color-primary-strong);
    block-size: 1.5em;
    border-radius: var(--loam-radius-full);
    color: var(--loam-color-on-strong);
    display: inline grid;
    flex-shrink: 0;
    inline-size: 1.5em;
    place-items: center;

    svg {
      block-size: 65%;
      inline-size: 65%;
    }

    @media (prefers-reduced-motion: no-preference) {
      transition: translate var(--loam-duration-md) var(--loam-ease-elastic);
    }
  }

  @media (hover: hover) {
    :scope:hover span.label {
      text-decoration-color: var(--loam-color-primary);
      text-decoration-thickness: max(0.16em, 2px);
      text-underline-offset: 0.3em;
    }

    :scope:hover span.icon {
      translate: 0.15em 0;
    }
  }
}
}

/* Skeleton */
@layer loamui.components {
/* The children (a LoamUI part or a consumer's own markup) are foreign
   content behind the donut; nothing here needs to reach them, because the
   whole subtree is hidden by inheritance from the root. */
@scope (.loam-Skeleton) to ([class*="loam-"]) {
  :scope {
    /* Public sizing hooks for a bare placeholder, where there is no content
       to measure; a wrapped child sizes the box instead (below). */
    block-size: var(--loam-skeleton-block-size, 1lh);

    /* Public shape hook — consumers may set it per instance or region. */
    border-radius: var(--loam-skeleton-radius, var(--loam-radius-md));
    display: block flow;
    inline-size: var(--loam-skeleton-inline-size, 100%);
    overflow: clip;
    pointer-events: none;
    position: relative;
    user-select: none;

    /* Everything inside keeps its layout but paints nothing: visibility
       inherits through the donut, where a child rule could not reach. */
    visibility: hidden;
  }

  /* Wrapped children size the box: measured, not declared. */
  :scope:has(> *) {
    block-size: var(--loam-skeleton-block-size, auto);
    inline-size: var(--loam-skeleton-inline-size, fit-content);
  }

  /* A wrapped Avatar is the circle this stands in for. */
  :scope:has(> .loam-Avatar) {
    border-radius: var(--loam-skeleton-radius, var(--loam-radius-full));
  }

  /* The tint is a weak mix of the text colour: text is guaranteed to
     contrast with any surface, so the skeleton stays visible on cards,
     stages and the page in both schemes. The gradient is static by
     default; the moving shimmer is opt-in motion (below), so
     reduced-motion users need no override. */
  :scope::after {
    background-color: color-mix(in oklab, var(--loam-color-fg) 8%, transparent);
    background-image: linear-gradient(
      90deg,
      transparent 0%,
      color-mix(in oklab, var(--loam-color-fg) 7%, transparent) 50%,
      transparent 100%
    );
    content: "";
    inset: 0;
    position: absolute;
    visibility: visible;

    @media (prefers-reduced-motion: no-preference) {
      animation: loam-skeleton-shimmer calc(var(--loam-duration-lg) * 5) var(--loam-ease) infinite;
      background-size: 200% 100%;
    }

    /* Background paint is dropped in forced colors; a GrayText outline keeps
       the placeholder's footprint visible. */
    @media (forced-colors: active) {
      border: 1px solid GrayText;
    }
  }
}

/* Keyframes are not scope-able; they stay at the top level. */
@keyframes loam-skeleton-shimmer {
  from {
    background-position: 200% 0;
  }

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

/* SkipLink */
@layer loamui.components {
@scope (.loam-SkipLink) {
  /* Fixed placement keeps the link out of flow in both states, so
     appearing on focus never shifts the page; z-index above every
     overlay. */
  :scope {
    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line-strong);
    border-radius: var(--loam-radius-md);
    box-shadow: var(--loam-shadow-lg);
    color: var(--loam-color-fg);
    font-size: var(--loam-text-sm);
    font-weight: 600;
    inset-block-start: var(--loam-space-2xs);
    inset-inline-start: var(--loam-space-2xs);
    padding-block: var(--loam-space-2xs);
    padding-inline: var(--loam-space-s);
    position: fixed;
    text-decoration: none;
    z-index: var(--loam-z-skip-link);

    /* Hidden is the unfocused state: :focus, not :focus-visible, because
       assistive tech can focus without the keyboard heuristics and the
       link must appear for every focus. These are the .loam-VisuallyHidden
       declarations (elements.css) behind that gate; the shared class has
       no gate, and undoing it on :focus would restate every property in
       reverse. */
    &:not(:focus) {
      block-size: 1px;
      border: 0;
      clip-path: inset(50%);
      inline-size: 1px;
      overflow: clip;
      padding: 0;
      white-space: nowrap;
    }
  }
}
}

/* Stepper */
@layer loamui.components {
/* A stepper is a region: it declares its container so the fluid tokens
   answer its width and the steps know whether there is room for a row,
   and each step hosts consumer content (a link in a title, an icon in a
   marker), hence the donut. The ol carries the order and the Step the
   state, in hidden words; the marker is drawn and hidden. The marker's
   diameter is the public --loam-stepper-marker-size, twice the body text
   by default so it rides the fluid scale. */
@scope (.loam-Stepper) to ([class*="loam-"]) {
  :scope {
    --_marker-size: var(--loam-stepper-marker-size, calc(2 * var(--loam-text-md)));
    --_gap: var(--loam-space-s);

    container-type: inline-size;
    counter-reset: step;
    display: block flex;
    flex-wrap: wrap;
    gap: var(--_gap);
    list-style: none;
    margin: 0;
    padding: 0;
  }

  /* One step: the marker in a column of its own, spanning the title and
     the description beside it. Each step takes the whole row, so the list
     stacks; the row layout below changes only the flex basis. */
  > li {
    counter-increment: step;
    display: block grid;
    flex: 1 1 100%;
    gap: var(--loam-space-3xs) var(--loam-space-xs);
    grid-template:
      "marker title" max-content
      "marker description" 1fr
      / max-content minmax(0, 1fr);
    margin: 0;
    min-inline-size: 0;

    /* The connector: a hairline from this marker to the next. It shares
       the marker's cell, centred on the column whatever the marker's
       width, and its negative end margin runs it across the list's gap.
       The last step has nothing to connect to. */
    &::after {
      border-inline-start: 1px solid var(--loam-color-line-strong);
      content: "";
      grid-area: marker;
      justify-self: center;
      margin-block: var(--_marker-size) calc(-1 * var(--_gap));
    }

    &:last-child::after {
      content: none;
    }
  }

  /* The circle. A marker holding more than a digit or two grows along
     the inline axis into a pill. Strong fill with on-strong text: the raw
     hue is never text. */
  span.marker {
    align-items: center;
    align-self: start;
    background: var(--loam-color-primary-strong);
    block-size: var(--_marker-size);
    border-radius: var(--loam-radius-full);
    color: var(--loam-color-on-strong);
    display: block flex;
    font-size: var(--loam-text-sm);
    font-variant-numeric: lining-nums tabular-nums;
    font-weight: 600;
    grid-area: marker;
    justify-content: center;
    line-height: 1;
    min-inline-size: var(--_marker-size);
    padding-inline: var(--loam-space-2xs);
    white-space: nowrap;

    /* The number is drawn only into an empty marker; children take its place. */
    &:empty::before {
      content: counter(step);
    }

    svg {
      block-size: auto;
      inline-size: 1em;
    }
  }

  /* .title, not span.title: the part keeps its styling when rendered as
     a heading or a link. */
  .title {
    color: var(--loam-color-fg);
    font-size: var(--loam-text-md);
    font-weight: 600;
    grid-area: title;
    margin: 0;
  }

  p.description {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);
    grid-area: description;
    margin: 0;
    max-inline-size: var(--loam-measure);
  }

  /* Progress, read from the one attribute the consumer sets. Steps before
     the current one are complete: their connector goes strong and an
     empty marker shows a check in place of its number (a background cut
     to shape, so it survives forced colours). The current step's marker
     gains a ring. Steps after it are upcoming: hollow markers, muted
     titles, the connector left as a line. */
  > li:has(~ li[aria-current="step"]) {
    &::after {
      border-color: var(--loam-color-primary-strong);
    }

    > span.marker:empty::before {
      background: currentcolor;
      block-size: 0.8em;
      clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%);
      content: "";
      inline-size: 0.8em;
    }
  }

  > li[aria-current="step"] {
    > span.marker {
      outline: 2px solid var(--loam-color-primary-strong);
      outline-offset: 2px;
    }

    .title {
      color: var(--loam-color-fg-strong);
    }
  }

  > li[aria-current="step"] ~ li {
    > span.marker {
      background: var(--loam-color-surface);
      border: 1px solid var(--loam-color-line-strong);
      color: var(--loam-color-fg-muted);
    }

    .title {
      color: var(--loam-color-fg-muted);
    }
  }

  /* The words a screen reader hears for the state, chosen by the same
     :has() that paints it. Every step carries both; the wrong one is kept
     out of the accessibility tree with display: none, and the right one
     is a box-less wrapper around the .loam-VisuallyHidden span inside it
     (past the donut, so it keeps the element style). The server HTML is
     right before any script runs. Two rules under exclusive conditions,
     not a base and an override. */
  > li:has(~ li[aria-current="step"]) > span.complete,
  > li[aria-current="step"] > span.current {
    display: contents;
  }

  > li:not(:has(~ li[aria-current="step"])) > span.complete,
  > li:not([aria-current="step"]) > span.current {
    display: none;
  }

  /* A row when there is room: each step sits beside the last, its marker
     over its title, and the connector runs along the marker's row to the
     next step's marker. */
  @container (inline-size >= 40rem) {
    > li {
      flex: 1 1 0;
      grid-template:
        "marker line" max-content
        "title title" max-content
        "description description" 1fr
        / max-content minmax(0, 1fr);

      &::after {
        border-block-start: 1px solid var(--loam-color-line-strong);
        border-inline-start: none;
        grid-area: line;
        margin-block: 0;
        margin-inline: 0 calc(-1 * var(--_gap));
        place-self: center stretch;
      }
    }

    > li:has(~ li[aria-current="step"])::after {
      border-color: var(--loam-color-primary-strong);
    }
  }

  /* Forced colours: the fills are background-painted and would vanish, so
     every marker keeps a border and the check keeps its own paint. The ring
     on the current step is an outline and survives on its own; an upcoming
     step's marker and the connectors from the current step on go dashed,
     so progress reads without the fills. */
  @media (forced-colors: active) {
    span.marker {
      border: 1px solid CanvasText;
    }

    > li {
      &::after {
        border-color: CanvasText;
      }

      &:has(~ li[aria-current="step"]) > span.marker:empty::before {
        forced-color-adjust: none;
      }
    }

    > li[aria-current="step"] {
      &::after {
        border-style: dashed;
      }

      ~ li {
        &::after,
        > span.marker {
          border-style: dashed;
        }
      }
    }
  }
}
}

/* Switch */
@layer loamui.components {
@scope (.loam-Switch-control) to ([class*="loam-"]) {
  :scope {
    --_track-w: 2.7778em;
    --_track-h: 1.5278em;
    --_gap: 0.2083em;

    align-items: center;
    cursor: pointer;
    display: inline flex;
    font-size: var(--loam-text-sm);
    position: relative;
  }

  /* Visually hide the native control but keep it in the a11y tree &
     focusable. The hit area is at least 24px (WCAG 2.5.8) even when the
     visible track is smaller — centred, invisible, no layout impact. */
  input {
    block-size: max(100%, 24px);
    cursor: inherit;
    inline-size: max(100%, 24px);
    inset-block-start: 50%;
    inset-inline-start: 50%;
    margin: 0;
    opacity: 0;
    position: absolute;
    translate: -50% -50%;
  }

  span.track {
    background: var(--loam-color-line-strong);
    block-size: var(--_track-h);
    border-radius: var(--loam-radius-full);
    flex-shrink: 0;
    inline-size: var(--_track-w);
    padding: var(--_gap);
    position: relative;

    @media (prefers-reduced-motion: no-preference) {
      transition:
        background var(--loam-duration-sm) var(--loam-ease),
        box-shadow var(--loam-duration-md) var(--loam-ease);
    }
  }

  span.thumb {
    background: var(--loam-color-on-strong);
    block-size: calc(var(--_track-h) - 2 * var(--_gap));
    border-radius: var(--loam-radius-full);
    box-shadow: var(--loam-shadow-sm);
    display: block flow;
    inline-size: calc(var(--_track-h) - 2 * var(--_gap));

    @media (prefers-reduced-motion: no-preference) {
      transition: translate var(--loam-duration-md) var(--loam-ease-elastic);
    }
  }

  input:checked + span.track {
    /* -strong, not raw primary: in a --loam-context region the fill becomes
       that status's audited -strong colour, so the on-strong thumb keeps 3:1
       (WCAG 1.4.11) in every context — raw warning could not. */
    background: var(--loam-color-primary-strong);

    span.thumb {
      translate: calc(var(--_track-w) - var(--_track-h)) 0;
    }
  }

  input:disabled + span.track {
    opacity: var(--loam-disabled-opacity);
  }

  /* The ring belongs on the track, not on the invisible hit-area input. */
  input:focus-visible {
    outline: none;

    + span.track {
      outline: var(--loam-ring-width) solid var(--loam-color-ring);
      outline-offset: 2px;
    }
  }

  /* Invalid after a composed Field error or a native submit attempt. */
  input[aria-invalid="true"] + span.track {
    --loam-color-ring: var(--loam-color-danger-strong);

    box-shadow: inset 0 0 0 1px var(--loam-color-danger);
  }

  /* The wrapper ancestor sits outside the scope root; the subject (the
     track) stays in scope. */
  @media (hover: hover) {
    :scope:hover input:not(:disabled) + span.track {
      background: color-mix(in oklch, var(--loam-color-line-strong) 80%, var(--loam-color-primary));
    }
  }

  /* Forced colors strips backgrounds — without this the switch is invisible.
     System colours keep the user's palette: ButtonText chrome, Highlight for
     the on state (thumb position still moves as the second signal). */
  @media (forced-colors: active) {
    span.track {
      border: 1px solid ButtonText;
    }

    span.thumb {
      background: ButtonText;
      forced-color-adjust: none;
    }

    input:checked + span.track {
      background: Highlight;
      border-color: Highlight;
      forced-color-adjust: none;

      span.thumb {
        background: HighlightText;
      }
    }

    /* The invalid inset shadow is dropped in forced colors; carry it as an
       outline in a system colour instead. */
    input[aria-invalid="true"] + span.track {
      outline: 2px solid LinkText;
      outline-offset: 1px;
    }

    input[aria-invalid="true"]:focus-visible + span.track {
      outline: var(--loam-ring-width) solid Highlight;
      outline-offset: 2px;
    }
  }
}
}

/* Table */
@layer loamui.components {
@scope (.loam-Table) to ([class*="loam-"]) {
  :scope {
    border-radius: var(--loam-radius-md);
    inline-size: 100%;

    /* A cap on the scroller's height is the public --loam-table-block-size:
       set it and long tables scroll in place, the wrapper becoming a named
       region as it does for width. Unset, the table is as tall as its rows. */
    max-block-size: var(--loam-table-block-size, none);

    /* overflow (shorthand), not the logical overflow-inline: the latter is
       not yet Baseline (see elements.css `pre`), so wide tables would not
       scroll in some browsers. */
    overflow: auto;

    /* A containing block for hidden text: a .loam-VisuallyHidden span inside
       a cell is positioned, and without this it escapes the scroller and
       widens the page. */
    position: relative;
  }

  table {
    font-size: var(--loam-text-sm);
    inline-size: 100%;
  }

  caption {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-xs);
    padding-block: var(--loam-space-2xs);
    text-align: start;
  }

  th,
  td {
    border-block-end: 1px solid var(--loam-color-line);

    /* The page hyphenates prose; a squeezed data cell should wrap at spaces
       or scroll, never break "desk-top" or "HW-1042". */
    hyphens: manual;
    padding-block: var(--loam-space-xs);
    padding-inline: var(--loam-space-s);
    vertical-align: middle;
  }

  thead th {
    border-block-end: 1px solid var(--loam-color-line-strong);
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-xs);
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    white-space: nowrap;
  }

  tbody tr:last-child :is(td, th) {
    border-block-end: none;
  }

  /* A sortable header holds a Button, which brings its own padding; the
     cell gives some of its own back so the row stays near header height. */
  thead th[aria-sort] {
    padding-block: var(--loam-space-3xs);
  }

  /* A sticky header: the column names stay at the top of the scroller (the
     wrapper, capped by --loam-table-block-size or the layout around it)
     while the rows pass beneath. The cells are painted an opaque surface
     so rows do not show through, and raised above the positioned hidden
     text a cell may hold. Collapsed borders belong to the table, not the
     cell, so the header's bottom line would stay behind as it sticks; the
     table separates them (at zero spacing, so nothing else moves) and each
     cell carries its own edge, which travels with it. A border rather than
     a shadow so forced colours, which drop shadows and flatten the
     background to Canvas, keep the line and the cover. */
  :scope[data-sticky-header] {
    table {
      border-collapse: separate;
      border-spacing: 0;
    }

    thead th {
      background: var(--loam-color-surface);
      inset-block-start: 0;
      position: sticky;
      z-index: 1;
    }
  }

  :scope[data-col-borders] th:not(:last-child),
  :scope[data-col-borders] td:not(:last-child) {
    border-inline-end: 1px solid var(--loam-color-line);
  }

  :scope[data-striped] tbody tr:nth-child(even) {
    background: var(--loam-color-bg-subtle);
  }

  @media (prefers-reduced-motion: no-preference) {
    :scope[data-hover] tbody tr {
      transition: background var(--loam-duration-md) var(--loam-ease);
    }
  }

  @media (hover: hover) {
    :scope[data-hover] tbody tr:hover {
      background: var(--loam-color-surface-hover);
    }
  }
}

/* The sort control is a core Button, past the table's donut: this scope
   adds only the glyph, read from the `aria-sort` on the cell around it.
   Text arrows in the button's own colour: the unsorted state is the
   two-headed shape, not a dimmer paint, so the glyph holds the button's
   contrast in every context and forced colours paint it as text. */
@scope (.loam-Table-sort) {
  :scope::after {
    content: "\2195";
    margin-inline-start: var(--loam-space-3xs);
  }

  th[aria-sort="ascending"] > :scope::after {
    content: "\2191";
  }

  th[aria-sort="descending"] > :scope::after {
    content: "\2193";
  }
}
}

/* Tabs */
@layer loamui.components {
@scope (.loam-Tabs) to ([class*="loam-"]) {
  :scope {
    display: block flex;
    flex-direction: column;
    inline-size: 100%;
  }

  /* The panel inherits the page's type: consumer content keeps the
     elements layer's size and leading. */
  div.panel:focus-visible {
    border-radius: var(--loam-radius-sm);
  }

  /* Boolean hidden only — [hidden="until-found"] must keep the UA's
     content-visibility treatment or find-in-page reveal breaks. */
  div.panel[hidden]:not([hidden="until-found"]) {
    display: none;
  }
}

/* The list carries its own root class, which the donut above excludes, so
   it takes its own scope. */
@scope (.loam-Tabs-list) to ([class*="loam-"]) {
  :scope {
    border-block-end: 1px solid var(--loam-color-line);
    display: block flex;
    flex-wrap: wrap;
    gap: var(--loam-space-3xs);
    margin-block-end: var(--loam-space-s);
  }

  button.tab {
    align-items: center;
    background: transparent;
    border: none;
    border-block-end: 2px solid transparent;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
    border-start-end-radius: var(--loam-radius-sm);
    border-start-start-radius: var(--loam-radius-sm);
    box-shadow: none;
    color: var(--loam-color-fg-muted);
    display: inline flex;
    font-family: inherit;
    font-size: var(--loam-text-sm);
    font-weight: 600;
    gap: var(--loam-space-2xs);
    line-height: 1;
    margin-block-end: -1px;
    padding-block: var(--loam-space-xs);
    padding-inline: var(--loam-space-s);
    position: relative;

    &[aria-disabled="true"] {
      cursor: default;
      opacity: 0.5;
    }

    /* The selected state is the ARIA state — detected, never duplicated as a
       data attribute. Text uses the audited -strong so it holds 4.5:1 in a
       --loam-context region (raw warning would not). */
    &[aria-selected="true"] {
      border-block-end-color: var(--loam-color-primary-strong);
      color: var(--loam-color-primary-strong);
    }

    &:has(svg) {
      align-items: center;
      gap: 0.45em;

      svg {
        block-size: 1.1em;
        inline-size: 1.1em;
      }
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        color var(--loam-duration-sm) var(--loam-ease),
        border-color var(--loam-duration-sm) var(--loam-ease),
        background var(--loam-duration-sm) var(--loam-ease);
    }

    @media (hover: hover) {
      &:hover:not([aria-disabled="true"]) {
        background: color-mix(in oklch, var(--loam-color-primary-soft) 50%, transparent);
        color: var(--loam-color-fg);
      }
    }
  }

  /* Forced colors: guarantee the active underline with a system colour.
     Must come after the base rule — same specificity, order decides.
     aria-disabled (unlike :disabled) gets no automatic GrayText, so name it. */
  @media (forced-colors: active) {
    button.tab[aria-selected="true"] {
      border-block-end-color: Highlight;
    }

    button.tab[aria-disabled="true"] {
      color: GrayText;
    }
  }
}
}

/* Textarea */
@layer loamui.components {
@scope (.loam-Textarea-field) to ([class*="loam-"]) {
  :scope {
    /* The inner control's curve, derived here because --_radius does not
       inherit: computed on the root, handed down as an ordinary property. */
    --_inner-radius: calc(var(--_radius, var(--loam-radius-md)) - 1px);

    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line-strong);
    border-radius: var(--_radius, var(--loam-radius-md));
    display: block flex;
    font-size: var(--loam-text-md);
    line-height: 1.5;

    &:focus-within {
      border-color: var(--loam-color-primary);
      outline: var(--loam-ring-width) solid var(--loam-color-ring);
      outline-offset: 2px;
    }

    @media (prefers-reduced-motion: no-preference) {
      transition:
        border-color var(--loam-duration-sm) var(--loam-ease),
        box-shadow var(--loam-duration-md) var(--loam-ease);
    }
  }

  /* Autofilled: the wrapper border carries the signal (the textarea itself
     paints the tint — see below). :autofill and :-webkit-autofill must stay
     in separate blocks — one unsupported selector invalidates a shared
     list. Declared before the error state so danger wins at equal
     specificity. */
  :scope:has(textarea:autofill) {
    border-color: color-mix(in oklab, var(--loam-color-primary) 50%, var(--loam-color-line-strong));
  }

  :scope:has(textarea:-webkit-autofill) {
    border-color: color-mix(in oklab, var(--loam-color-primary) 50%, var(--loam-color-line-strong));
  }

  :scope:has(textarea[aria-invalid="true"]) {
    --loam-color-ring: var(--loam-color-danger-strong);

    border-color: var(--loam-color-danger);
  }

  :scope:has(textarea:disabled) {
    background: var(--loam-color-bg-subtle);
    opacity: var(--loam-disabled-opacity);
  }

  /* The height is the rows attribute's, as the platform sizes it; nothing
     here declares a floor of its own. */
  textarea {
    background: transparent;
    border: none;

    /* Follows the wrapper's inner curve, one border-width tighter (see the
       root). Read from the inherited private property, not from --_radius:
       that one is registered inherits: false, so a child always falls back to
       radius-md and would keep the wrong curve the moment a consumer set it. */
    border-radius: var(--_inner-radius);
    color: inherit;
    flex: 1;
    font: inherit;
    min-inline-size: 0;
    outline: none;
    padding: var(--loam-space-xs);
    resize: block;
  }

  textarea::placeholder {
    color: var(--loam-color-fg-dim);
  }

  /* background-color cannot be overridden on an autofilled field; an inset
     shadow wider than the control paints the tint instead. Separate blocks —
     see the wrapper rules above. */
  textarea:autofill {
    box-shadow: inset 0 0 0 100px
      color-mix(in oklab, var(--loam-color-primary) 8%, var(--loam-color-surface));
  }

  textarea:-webkit-autofill {
    box-shadow: inset 0 0 0 100px
      color-mix(in oklab, var(--loam-color-primary) 8%, var(--loam-color-surface));
  }

  textarea:disabled {
    resize: none;
  }

  /* Native auto-grow where the platform provides it. field-sizing ignores
     the rows attribute, so the component mirrors it into --_rows (the
     fallback is the attribute's own default): the field starts at that
     many lines and grows to ten, or to its rows when asked for more. */
  @supports (field-sizing: content) {
    textarea {
      field-sizing: content;
      max-block-size: calc(max(10lh, var(--_rows, 3) * 1lh) + var(--loam-space-xs) * 2);
      min-block-size: calc(var(--_rows, 3) * 1lh + var(--loam-space-xs) * 2);
    }
  }

  /* Forced colours drop the danger border colour, so the invalid state
     moves to an outline in a system colour; the focus ring is offset
     further out and still reads as its own ring. Mirrors Input. */
  @media (forced-colors: active) {
    :scope:has(textarea[aria-invalid="true"]) {
      outline: 2px solid LinkText;
      outline-offset: 1px;

      &:focus-within {
        outline: var(--loam-ring-width) solid Highlight;
        outline-offset: 2px;
      }
    }
  }
}
}

/* Time */
@layer loamui.components {
/*
 * A Time sizes nothing: the date takes the font of whatever it sits in, and
 * its figures (lining, tabular) are the `time` element rule in elements.css,
 * so plain markup gets them too. Nothing is left for the component to add.
 */
}

/* Toast */
@layer loamui.components {
/* The viewport lives in the top layer (popover="manual"), so it carries its
   own scope; toasts stack from the block-end corner. */
@scope (.loam-Toast-viewport) to ([class*="loam-"]) {
  :scope {
    background: none;
    border: 0;
    display: block flex;
    flex-direction: column;
    gap: var(--loam-space-2xs);
    inline-size: min(var(--loam-toast-size, 22rem), calc(100vi - 2 * var(--loam-space-s)));

    /* Clear the UA's [popover] inset/size defaults. */
    inset: auto;
    inset-block-end: var(--loam-space-s);
    inset-inline-end: var(--loam-space-s);
    margin: 0;
    outline: none;
    overflow: visible;
    padding: 0;
    position: fixed;
  }

  /* No toasts: don't intercept clicks in the corner. */
  :scope[data-empty] {
    pointer-events: none;
  }

  div.toast {
    align-items: flex-start;
    background: var(--loam-color-surface);
    border: 1px solid var(--loam-color-line);
    border-radius: var(--loam-radius-md);
    box-shadow: var(--loam-shadow-lg);
    display: block flex;
    gap: var(--loam-space-2xs);
    padding: var(--loam-space-xs);

    @media (prefers-reduced-motion: no-preference) {
      transition:
        opacity var(--loam-duration-lg) var(--loam-ease),
        translate var(--loam-duration-lg) var(--loam-ease);

      @starting-style {
        opacity: 0;
        translate: 0 0.5rem;
      }
    }
  }

  /* Exit: the provider marks the toast, plays this, then removes it. */
  div.toast[data-exiting] {
    opacity: 0;
    translate: 0 0.5rem;
  }

  div.content {
    flex: 1;
    min-inline-size: 0;
  }

  div.title {
    font-size: var(--loam-text-sm);
    font-weight: 600;
  }

  div.description {
    color: var(--loam-color-fg-muted);
    font-size: var(--loam-text-sm);
    overflow-wrap: break-word;

    div.title + & {
      margin-block-start: var(--loam-space-3xs);
    }
  }

  @supports not selector(:popover-open) {
    :scope {
      z-index: var(--loam-z-toast);
    }
  }
}
}

/* Tooltip */
@layer loamui.components {
@scope (.loam-Tooltip) {
  :scope {
    display: inline flow-root;
    position: relative;
  }
}

/* The bubble escapes into the top layer when enhanced, so it carries its
   own scope. */
@scope (.loam-Tooltip-popup) to ([class*="loam-"]) {
  :scope {
    --_gap: var(--loam-space-3xs);
    --_arrow: 0.4em;

    background: var(--loam-color-fg);
    border: 0;
    border-radius: var(--loam-radius-sm);
    box-shadow: var(--loam-shadow-md);
    color: var(--loam-color-bg);
    font-size: var(--loam-text-xs);
    inline-size: max-content;
    margin: 0;
    max-inline-size: var(--loam-tooltip-size, 16rem);

    /* The UA gives [popover] `overflow: auto`, which would clip the arrow
       poking outside the bubble. Tooltips never scroll. */
    overflow: visible;
    padding-block: var(--loam-space-3xs);
    padding-inline: var(--loam-space-2xs);
    text-align: center;
    white-space: normal;

    /* Shadows vanish in forced colors; a border keeps the bubble's edge. */
    @media (forced-colors: active) {
      border: 1px solid;
    }

    /* Enhanced: top layer + anchor positioning. position-anchor /
       anchor-name are set from inline styles with a per-instance name
       shared by Trigger and Popup. */
    @supports (anchor-name: --loam-probe) {
      /* stylelint-disable-next-line declaration-property-value-no-unknown --
         lets the arrow query which position-try fallback applied (Chrome
         143+; inert elsewhere); stylelint's CSS data doesn't know the
         value yet. */
      container-type: anchored;

      /* The UA gives [popover] `inset: 0`; clear it so only the insets each
         branch sets take part, otherwise the box is over-constrained. */
      inset: auto;
      position: fixed;

      &[data-side="top"] {
        inset-block-end: calc(anchor(start) + var(--_gap));
        justify-self: anchor-center;
        position-try: flip-block;
      }

      &[data-side="bottom"] {
        inset-block-start: calc(anchor(end) + var(--_gap));
        justify-self: anchor-center;
        position-try: flip-block;
      }

      &[data-side="left"] {
        align-self: anchor-center;
        inset-inline-end: calc(anchor(start) + var(--_gap));
        position-try: flip-inline;
      }

      &[data-side="right"] {
        align-self: anchor-center;
        inset-inline-start: calc(anchor(end) + var(--_gap));
        position-try: flip-inline;
      }

      @media (prefers-reduced-motion: no-preference) {
        opacity: 1;
        scale: 1;
        transition:
          opacity var(--loam-duration-lg) var(--loam-ease),
          scale var(--loam-duration-lg) var(--loam-ease),
          display var(--loam-duration-lg) var(--loam-ease) allow-discrete,
          overlay var(--loam-duration-lg) var(--loam-ease) allow-discrete;

        /* Exit: transition back to the closed state (see Modal). */
        &:not(:popover-open) {
          opacity: 0;
          scale: 0.96;
        }

        @starting-style {
          opacity: 0;
          scale: 0.96;
        }
      }
    }

    /* Fallback: anchored to the wrapper. */
    @supports not (anchor-name: --loam-probe) {
      position: absolute;
      z-index: var(--loam-z-popup);

      /* translate is physical, inset-inline-* is flow-relative — the
         centering shift must follow the text direction. */
      &[data-side="top"] {
        inset-block-end: calc(100% + var(--_gap));
        inset-inline-start: 50%;

        &:dir(ltr) {
          translate: -50% 0;
        }

        &:dir(rtl) {
          translate: 50% 0;
        }
      }

      &[data-side="bottom"] {
        inset-block-start: calc(100% + var(--_gap));
        inset-inline-start: 50%;

        &:dir(ltr) {
          translate: -50% 0;
        }

        &:dir(rtl) {
          translate: 50% 0;
        }
      }

      &[data-side="left"] {
        inset-block-start: 50%;
        inset-inline-end: calc(100% + var(--_gap));
        translate: 0 -50%;
      }

      &[data-side="right"] {
        inset-block-start: 50%;
        inset-inline-start: calc(100% + var(--_gap));
        translate: 0 -50%;
      }
    }
  }

  span.arrow {
    block-size: 0;
    border: var(--_arrow) solid transparent;
    inline-size: 0;
    position: absolute;

    :scope[data-side="top"] & {
      border-block-start-color: var(--loam-color-fg);
      inset-block-start: 100%;
      inset-inline-start: calc(50% - var(--_arrow));
    }

    :scope[data-side="bottom"] & {
      border-block-end-color: var(--loam-color-fg);
      inset-block-end: 100%;
      inset-inline-start: calc(50% - var(--_arrow));
    }

    :scope[data-side="left"] & {
      border-inline-start-color: var(--loam-color-fg);
      inset-block-start: calc(50% - var(--_arrow));
      inset-inline-start: 100%;
    }

    :scope[data-side="right"] & {
      border-inline-end-color: var(--loam-color-fg);
      inset-block-start: calc(50% - var(--_arrow));
      inset-inline-end: 100%;
    }

    /* When position-try flipped the bubble, the arrow follows: it moves to
       the opposite edge and points back at the trigger. Anchored container
       queries are the only CSS signal for the applied fallback, so these
       are inherently overrides of the requested side; browsers without
       them keep the requested-side arrow. */
    @container anchored(fallback: flip-block) {
      :scope[data-side="top"] & {
        border-block-end-color: var(--loam-color-fg);
        border-block-start-color: transparent;
        inset-block: auto 100%;
      }

      :scope[data-side="bottom"] & {
        border-block-end-color: transparent;
        border-block-start-color: var(--loam-color-fg);
        inset-block: 100% auto;
      }
    }

    @container anchored(fallback: flip-inline) {
      :scope[data-side="left"] & {
        border-inline-end-color: var(--loam-color-fg);
        border-inline-start-color: transparent;
        inset-inline: auto 100%;
      }

      :scope[data-side="right"] & {
        border-inline-end-color: transparent;
        border-inline-start-color: var(--loam-color-fg);
        inset-inline: 100% auto;
      }
    }
  }
}
}

/* VisuallyHidden */
@layer loamui.components {
@scope (.loam-VisuallyHidden) {
  /* One pixel, clipped, still rendered: a screen reader reaches text that is
     in the layout, and display: none or visibility: hidden would take it out
     of the accessibility tree along with the eye. The negative margin keeps
     that pixel from nudging anything, and nowrap stops a long name being
     broken one character per line before it is clipped. */
  :scope {
    block-size: 1px;
    border: 0;
    clip-path: inset(50%);
    inline-size: 1px;
    margin: -1px;
    overflow: clip;
    padding: 0;
    position: absolute;
    white-space: nowrap;
  }
}
}
