/* Unbranded design system: components.css
 * Version 1.0.0. Link after tokens.css. Every rule is layered (ub.*); unlayered page CSS wins.
 * Grammar: one root class .ub-{component}; parts .ub-{component}-{part}; closed attributes data-variant,
 * data-tone, data-size, data-density on the root; state from native pseudo-classes and ARIA, never .is-* classes.
 * Annotations (@component ...) feed source/build.mjs, which writes registry.json and checks drift.
 */

/* ============================== reset ============================== */
@layer ub.reset {
  *, *::before, *::after { box-sizing: border-box; }
  * { margin: 0; }
  html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; scrollbar-gutter: stable; -webkit-tap-highlight-color: transparent; }
  :where(img, svg, video, canvas, picture) { display: block; max-inline-size: 100%; block-size: auto; }
  :where(input, button, select, textarea) { font: inherit; color: inherit; }
  :where(p, li, dd, dt, blockquote, figcaption, h1, h2, h3, h4, h5, h6) { overflow-wrap: break-word; }
  :where(button) { cursor: pointer; }
  :where([hidden]) { display: none !important; }
}

/* ============================== base ============================== */
@layer ub.base {
  body {
    min-block-size: 100vh; min-block-size: 100dvh;
    background: var(--ub-bg); color: var(--ub-fg);
    font-family: var(--ub-font-body); font-size: var(--ub-text-body); line-height: var(--ub-leading-body);
    font-weight: var(--ub-weight-body); font-optical-sizing: auto;
    -webkit-font-smoothing: antialiased;
  }
  :is(h1, h2, h3, h4, h5, h6) {
    font-family: var(--ub-font-display); font-weight: var(--ub-weight-heading); color: var(--ub-fg);
    text-wrap: balance; max-inline-size: var(--ub-measure-heading);
  }
  h1 { font-size: var(--ub-h1); line-height: var(--ub-leading-h1); letter-spacing: var(--ub-tracking-display); }
  h2 { font-size: var(--ub-h2); line-height: var(--ub-leading-h2); letter-spacing: var(--ub-tracking-h2); }
  h3 { font-size: var(--ub-h3); line-height: var(--ub-leading-h3); letter-spacing: var(--ub-tracking-h3); }
  h4 { font-size: var(--ub-h4); line-height: var(--ub-leading-h4); }
  :is(h5, h6) { font-size: var(--ub-text-base); line-height: var(--ub-leading-ui); font-family: var(--ub-font-ui); }
  :is(h1, h2, h3, h4, h5, h6) :is(em, i, cite) { font-style: normal; font-weight: inherit; }
  :is(p, li, dd) { text-wrap: pretty; max-inline-size: var(--ub-measure); }
  :is(p, li) { font-variant-numeric: proportional-nums; }
  :is(ul, ol) { padding-inline-start: 1.25em; }
  li + li { margin-block-start: 0.25em; }
  strong, b { font-weight: var(--ub-weight-strong); }
  small { font-size: var(--ub-text-sm); }

  a { color: var(--ub-accent-text); text-decoration-line: underline; text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
  @media (hover: hover) { a:hover { text-decoration-thickness: 2px; } }

  :focus-visible { outline: var(--ub-focus-width) solid var(--ub-focus); outline-offset: var(--ub-focus-offset); }
  ::selection { background: var(--ub-accent-subtle); color: var(--ub-fg); }

  hr { border: 0; border-block-start: 1px solid var(--ub-border); margin-block: var(--ub-block); }
  blockquote { padding-inline-start: var(--ub-space-4); border-inline-start: 1px solid var(--ub-border-strong); color: var(--ub-fg); }

  :where(code, kbd, samp, pre) { font-family: var(--ub-font-mono); font-size: 0.9em; }
  :where(:not(pre)) > code { background: var(--ub-surface-sunken); padding: 0.1em 0.3em; border-radius: var(--ub-radius-control); }
  kbd {
    display: inline-block; padding: 0 0.35em; border: 1px solid var(--ub-border-strong); border-block-end-width: 2px;
    border-radius: var(--ub-radius-control); background: var(--ub-surface-raised); font-size: var(--ub-text-xs); line-height: 1.6;
  }

  table {
    border-collapse: separate; border-spacing: 0; inline-size: auto;
    font-family: var(--ub-font-ui); font-size: var(--ub-text-sm); line-height: var(--ub-leading-ui);
    font-variant-numeric: tabular-nums;
  }
  caption { caption-side: top; text-align: start; font-weight: var(--ub-weight-strong); padding-block-end: var(--ub-space-2); color: var(--ub-fg); }
  th { text-align: start; font-weight: var(--ub-weight-strong); }
  thead th { white-space: nowrap; }
  :is(th, td) { padding: var(--ub-space-2) var(--ub-space-5) var(--ub-space-2) 0; vertical-align: baseline; border-block-end: 1px solid var(--ub-border); }
  :is(th, td):last-child { padding-inline-end: 0; }

  :where(input, select, textarea) { font-size: max(16px, 1em); }
  :where(button, select, input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="hidden"])) {
    line-height: var(--ub-leading-control); min-block-size: var(--ub-control-md);
  }
  :where(input[type="checkbox"], input[type="radio"]) { accent-color: var(--ub-accent-text); inline-size: 1.125rem; block-size: 1.125rem; }

  @media print {
    :where(nav, .ub-appbar, .ub-toast-region, .ub-skip, button) { display: none !important; }
    body { background: white; color: black; }
    a { color: inherit; }
  }
}

/* ============================== layout primitives ============================== */
@layer ub.layout {
  /** @component stack @summary Vertical rhythm for components and forms. @use Forms, panels, lists of controls. Never around prose (use flow). @knobs --ub-stack-space @since 1.0.0 */
  :where(.ub-stack) { display: flex; flex-direction: column; gap: var(--ub-stack-space, var(--ub-space-4)); }
  /** @component flow @summary Prose rhythm: space between blocks, more above headings than below. @use Articles and any running text. @knobs --ub-flow-space @since 1.0.0 */
  :where(.ub-flow) > * + * { margin-block-start: var(--ub-flow-space, 1em); }
  :where(.ub-flow) > * + :is(h2, h3, h4) { margin-block-start: var(--ub-heading-before); }
  :where(.ub-flow) > :is(h1, h2, h3, h4, h5, h6) + * { margin-block-start: var(--ub-heading-after); }
  /** @component box @summary A bordered padded area. @use Grouping without a title. Never inside another box or panel. @knobs --ub-box-pad @since 1.0.0 */
  :where(.ub-box) { padding: var(--ub-box-pad, var(--ub-space-4)); border: 1px solid var(--ub-border); border-radius: var(--ub-radius-surface); }
  /** @component center @summary Centered content column with the page gutter. @use Inside every full-bleed band or page. @knobs --ub-center-max @since 1.0.0 */
  :where(.ub-center) {
    box-sizing: content-box; max-inline-size: var(--ub-center-max, var(--ub-page-default)); margin-inline: auto;
    padding-inline: max(var(--ub-gutter), env(safe-area-inset-left)) max(var(--ub-gutter), env(safe-area-inset-right));
  }
  /** @component cluster @summary Wrapping row of items. @use Button groups, tags, inline metadata. @knobs --ub-cluster-space --ub-cluster-align --ub-cluster-justify @since 1.0.0 */
  :where(.ub-cluster) { display: flex; flex-wrap: wrap; gap: var(--ub-cluster-space, var(--ub-space-3));
    align-items: var(--ub-cluster-align, center); justify-content: var(--ub-cluster-justify, flex-start); }
  /** @component with-sidebar @summary Sidebar beside content that wraps under it when narrow. @use Marginalia, filters, page index. @parts ub-sidebar @knobs --ub-sidebar-width --ub-content-min @since 1.0.0 */
  :where(.ub-with-sidebar) { display: flex; flex-wrap: wrap; gap: var(--ub-gap); }
  :where(.ub-with-sidebar) > :where(.ub-sidebar) { flex-basis: var(--ub-sidebar-width, 16rem); flex-grow: 1; }
  :where(.ub-with-sidebar) > :where(:not(.ub-sidebar)) { flex-basis: 0; flex-grow: 999; min-inline-size: var(--ub-content-min, 55%); }
  /** @component switcher @summary Row that becomes a column below a container width. @use Two or three unlike blocks side by side. @knobs --ub-switcher-threshold @since 1.0.0 */
  :where(.ub-switcher) { display: flex; flex-wrap: wrap; gap: var(--ub-gap); }
  :where(.ub-switcher) > * { flex-grow: 1; flex-basis: calc((var(--ub-switcher-threshold, 40rem) - 100%) * 999); }
  :where(.ub-switcher) > :nth-last-child(n + 5), :where(.ub-switcher) > :nth-last-child(n + 5) ~ * { flex-basis: 100%; }
  /** @component cover @summary Fills the viewport height with one centered main block. @use Sign-in, empty app states, a title screen. @knobs --ub-cover-min @since 1.0.0 */
  :where(.ub-cover) { display: flex; flex-direction: column; gap: var(--ub-space-4);
    min-block-size: var(--ub-cover-min, 100vh); min-block-size: var(--ub-cover-min, 100svh); }
  :where(.ub-cover) > :where(.ub-cover-main) { margin-block: auto; }
  /** @component grid @summary Two-track grid with one dominant track (7/5); equal tracks only with data-fit. @use Claim beside evidence, content beside a figure. data-fit only for galleries of like items. @knobs --ub-grid-cols --ub-grid-min @since 1.0.0 */
  :where(.ub-grid) { display: grid; gap: var(--ub-gap); grid-template-columns: var(--ub-grid-cols, minmax(0, 7fr) minmax(0, 5fr)); }
  :where(.ub-grid[data-fit]) { grid-template-columns: repeat(auto-fill, minmax(min(var(--ub-grid-min, 16rem), 100%), 1fr)); }
  @media (width < 40rem) { :where(.ub-grid:not([data-fit])) { grid-template-columns: minmax(0, 1fr); } }
  /** @component frame @summary Crops media to an aspect ratio. @use Photos and screenshots in a fixed shape. @knobs --ub-frame-ratio @since 1.0.0 */
  :where(.ub-frame) { aspect-ratio: var(--ub-frame-ratio, 16 / 9); overflow: hidden; display: flex; justify-content: center; align-items: center; }
  :where(.ub-frame) > :is(img, video) { inline-size: 100%; block-size: 100%; object-fit: cover; }
  /** @component reel @summary Horizontal scroller. @use A strip of screenshots. Never for primary content. @knobs --ub-reel-item @since 1.0.0 */
  :where(.ub-reel) { display: flex; overflow-x: auto; gap: var(--ub-space-3); scroll-snap-type: x proximity; padding-block-end: var(--ub-space-2); }
  :where(.ub-reel) > * { flex: 0 0 var(--ub-reel-item, 18rem); scroll-snap-align: start; }
  /** @component with-icon @summary Icon aligned to the first line of text. @use Inline status text, links with an icon. @since 1.0.0 */
  :where(.ub-with-icon) { display: inline-flex; align-items: baseline; gap: 0.5ch; }
  :where(.ub-with-icon) > :is(svg, img) { inline-size: 1em; block-size: 1em; flex: none; align-self: center; }
  /** @component cq @summary Marks a container for container queries. @use Components that change layout by their own width. @since 1.0.0 */
  :where(.ub-cq) { container-type: inline-size; }
  /** @component table-wrap @summary Scroll container for wide tables. @use Every table that can exceed a phone's width. Add tabindex="0", role="region", aria-label. @knobs --ub-table-max-h @since 1.0.0 */
  :where(.ub-table-wrap) { overflow: auto; max-inline-size: 100%; max-block-size: var(--ub-table-max-h, none); }
}

/* ============================== components ============================== */
@layer ub.components {

  /* ---------- band: full-bleed section ---------- */
  /** @component band @summary Full-width page section that carries its own ground. @use Page sections. data-scheme="dark" on a light page makes an inverse band; data-ground="sunken" or "accent" changes the ground. @variants default | sunken | accent @since 1.0.0 */
  .ub-band { background: var(--ub-bg); color: var(--ub-fg); padding-block: var(--ub-section); }
  .ub-band[data-ground="sunken"] { background: var(--ub-surface-sunken); }
  .ub-band[data-ground="accent"] { background: var(--ub-accent-subtle); }
  .ub-band + .ub-band:not([data-ground], [data-scheme]) { border-block-start: 1px solid var(--ub-border); }

  /* ---------- display and lead text ---------- */
  /** @component display @summary Display-size headline for one short phrase (six words at most). @use The page's main headline on explainer and argument pages. @variants 4xl | 5xl @since 1.0.0 */
  .ub-display { font-family: var(--ub-font-display); font-weight: var(--ub-weight-heading); font-size: var(--ub-text-4xl);
    line-height: var(--ub-leading-display); letter-spacing: var(--ub-tracking-display); max-inline-size: 18ch; text-wrap: balance; }
  .ub-display[data-size="5xl"] { font-size: var(--ub-text-5xl); }
  /** @component lead @summary The paragraph that says what the page is for. @use Once, under the page headline. @since 1.0.0 */
  .ub-lead { font-size: var(--ub-text-lg); line-height: 1.5; color: var(--ub-fg); max-inline-size: 52ch; }

  /* ---------- icon ---------- */
  /** @component icon @summary Inline SVG icon from the page's Lucide sprite. @use Controls, status, navigation only. Never above a heading, never larger than 24px. @variants sm | md | lg @since 1.0.0 */
  .ub-icon { inline-size: var(--ub-icon-md); block-size: var(--ub-icon-md); flex: none; fill: none; stroke: currentColor;
    stroke-width: var(--ub-icon-stroke); stroke-linecap: round; stroke-linejoin: round; vertical-align: -0.2em; display: inline-block; }
  .ub-icon[data-size="sm"] { inline-size: var(--ub-icon-sm); block-size: var(--ub-icon-sm); }
  .ub-icon[data-size="lg"] { inline-size: var(--ub-icon-lg); block-size: var(--ub-icon-lg); }

  /* ---------- button ---------- */
  /** @component button @summary The action control. @use One solid accent button per view for the primary action; soft, outline, or ghost for the rest. @aliases btn, cta @variants solid | soft | outline | ghost | link @states default, hover, focus-visible, active, disabled, loading @since 1.0.0 */
  .ub-button {
    --_solid: var(--ub-accent); --_solid-hover: var(--ub-accent-hover); --_on: var(--ub-on-accent);
    --_subtle: var(--ub-accent-subtle); --_text: var(--ub-accent-text); --_line: var(--ub-accent-text);
    --_bg: var(--_solid); --_fg: var(--_on); --_bd: var(--_solid); --_bg-hover: var(--_solid-hover); --_bd-hover: var(--_solid-hover);
    display: inline-flex; align-items: center; justify-content: center; gap: var(--ub-space-2);
    min-block-size: var(--ub-control-md); padding-inline: var(--ub-space-4); padding-block: 0;
    font-family: var(--ub-font-ui); font-size: var(--ub-text-ui); font-weight: var(--ub-weight-medium); line-height: var(--ub-leading-control);
    color: var(--_fg); background: var(--_bg); border: 1px solid var(--_bd); border-radius: var(--ub-radius-control);
    text-decoration: none; white-space: nowrap; cursor: pointer; user-select: none;
    transition: background-color var(--ub-dur-fast) var(--ub-ease-out), border-color var(--ub-dur-fast) var(--ub-ease-out), color var(--ub-dur-fast) var(--ub-ease-out);
  }
  .ub-button[data-tone="neutral"] { --_solid: var(--ub-fg); --_solid-hover: var(--ub-neutral-11); --_on: var(--ub-bg);
    --_subtle: var(--ub-surface-sunken); --_text: var(--ub-fg); --_line: var(--ub-border-control); }
  .ub-button[data-tone="danger"] { --_solid: var(--ub-danger); --_solid-hover: var(--ub-danger-text); --_on: var(--ub-on-danger);
    --_subtle: var(--ub-danger-subtle); --_text: var(--ub-danger-text); --_line: var(--ub-danger-text); }
  .ub-button[data-variant="soft"] { --_bg: var(--_subtle); --_fg: var(--_text); --_bd: transparent; --_bg-hover: var(--_subtle); --_bd-hover: var(--_line); }
  .ub-button[data-variant="outline"] { --_bg: transparent; --_fg: var(--_text); --_bd: var(--_line); --_bg-hover: var(--_subtle); --_bd-hover: var(--_line); }
  .ub-button[data-variant="ghost"] { --_bg: transparent; --_fg: var(--_text); --_bd: transparent; --_bg-hover: var(--ub-surface-sunken); --_bd-hover: transparent; }
  .ub-button[data-variant="link"] { --_bg: transparent; --_fg: var(--_text); --_bd: transparent; --_bg-hover: transparent; --_bd-hover: transparent;
    padding-inline: 0; min-block-size: auto; text-decoration: underline; text-underline-offset: 0.18em; }
  .ub-button[data-size="sm"] { min-block-size: var(--ub-control-sm); padding-inline: var(--ub-space-3); font-size: var(--ub-text-sm); }
  .ub-button[data-size="lg"] { min-block-size: var(--ub-control-lg); padding-inline: var(--ub-space-5); font-size: var(--ub-text-base); }
  .ub-button[data-size="xs"] { min-block-size: var(--ub-control-xs); padding-inline: var(--ub-space-2); font-size: var(--ub-text-xs); gap: var(--ub-space-1); }
  @media (hover: hover) { .ub-button:hover { background: var(--_bg-hover); border-color: var(--_bd-hover); } }
  .ub-button[data-variant="link"]:hover { text-decoration-thickness: 2px; }
  .ub-button:active { translate: 0 1px; }
  .ub-button:is(:disabled, [aria-disabled="true"]) { --_bg: var(--ub-surface-sunken); --_fg: var(--ub-fg-faint); --_bd: var(--ub-border);
    --_bg-hover: var(--ub-surface-sunken); --_bd-hover: var(--ub-border); cursor: not-allowed; translate: none; }
  .ub-button[aria-busy="true"] { cursor: progress; }
  .ub-button[aria-pressed="true"] { --_bg: var(--_subtle); --_fg: var(--_text); --_bd: var(--_line); }

  /** @component icon-button @summary A button that shows only an icon. @use Toolbar and close actions. Requires aria-label. @aliases icon-btn @variants solid | soft | outline | ghost @states default, hover, focus-visible, active, disabled @since 1.0.0 */
  .ub-icon-button { --_bg: transparent; --_fg: var(--ub-fg-muted); --_bd: transparent; --_bg-hover: var(--ub-surface-sunken);
    display: inline-grid; place-items: center; inline-size: var(--ub-control-md); block-size: var(--ub-control-md); padding: 0;
    color: var(--_fg); background: var(--_bg); border: 1px solid var(--_bd); border-radius: var(--ub-radius-control); cursor: pointer;
    transition: background-color var(--ub-dur-fast) var(--ub-ease-out), color var(--ub-dur-fast) var(--ub-ease-out); }
  .ub-icon-button[data-variant="outline"] { --_bd: var(--ub-border-control); }
  .ub-icon-button[data-size="sm"] { inline-size: var(--ub-control-sm); block-size: var(--ub-control-sm); }
  @media (hover: hover) { .ub-icon-button:hover { background: var(--_bg-hover); color: var(--ub-fg); } }
  .ub-icon-button:active { translate: 0 1px; }
  .ub-icon-button:disabled { color: var(--ub-fg-faint); cursor: not-allowed; }

  /* ---------- forms ---------- */
  /** @component field @summary A label, a control, help text, and an error message. @use Every form control. The error shows once the control is invalid after interaction. @states default, error, success, disabled @since 1.0.0 */
  .ub-field { display: grid; gap: var(--ub-space-1); align-content: start; }
  .ub-field-label { font-family: var(--ub-font-ui); font-size: var(--ub-text-ui); font-weight: var(--ub-weight-medium); color: var(--ub-fg); }
  .ub-field-help { font-family: var(--ub-font-ui); font-size: var(--ub-text-sm); color: var(--ub-fg-muted); max-inline-size: var(--ub-measure-ui); }
  .ub-field-error { display: none; font-family: var(--ub-font-ui); font-size: var(--ub-text-sm); font-weight: var(--ub-weight-medium); color: var(--ub-danger-text); }
  .ub-field:has(:user-invalid, [aria-invalid="true"]) .ub-field-error { display: block; }

  /** @component input @summary Text input, select, or textarea. @use Inside a field. Inputs never render under 16px. @aliases text-field, textbox @states default, hover, focus-visible, disabled, read-only, error @since 1.0.0 */
  .ub-input {
    inline-size: 100%; min-block-size: var(--ub-control-md); padding: 0 var(--ub-space-3);
    font-family: var(--ub-font-ui); font-size: max(16px, var(--ub-text-ui)); line-height: var(--ub-leading-control); color: var(--ub-fg);
    background: var(--ub-surface-raised); border: 1px solid var(--ub-border-control); border-radius: var(--ub-radius-control);
    transition: border-color var(--ub-dur-fast) var(--ub-ease-out);
  }
  textarea.ub-input { padding-block: var(--ub-space-2); min-block-size: 6rem; line-height: var(--ub-leading-ui); resize: vertical; }
  textarea.ub-input[data-autosize] { field-sizing: content; }
  @media (hover: hover) { .ub-input:hover:not(:disabled, [readonly]) { border-color: var(--ub-fg-muted); } }
  .ub-input:focus-visible { border-color: var(--ub-focus); outline-offset: 0; }
  .ub-input::placeholder { color: var(--ub-fg-faint); }
  .ub-input:disabled { background: var(--ub-surface-sunken); color: var(--ub-fg-faint); cursor: not-allowed; }
  .ub-input[readonly] { background: var(--ub-surface-sunken); border-color: var(--ub-border); }
  .ub-input:is(:user-invalid, [aria-invalid="true"]) { border-color: var(--ub-danger-text); }

  /** @component check @summary Checkbox or radio with its label. @use Group related checks in a fieldset. @aliases checkbox, radio @states unchecked, checked, indeterminate, disabled, focus-visible @since 1.0.0 */
  .ub-check { inline-size: fit-content; max-inline-size: 100%; display: inline-flex; align-items: center; gap: var(--ub-space-2); font-family: var(--ub-font-ui); font-size: var(--ub-text-ui); cursor: pointer; }
  .ub-check:has(:disabled) { color: var(--ub-fg-faint); cursor: not-allowed; }

  /** @component switch @summary On and off for one setting that applies immediately. @use Settings. Never for a choice that needs a save button (use a checkbox). @aliases toggle @states off, on, disabled, focus-visible @since 1.0.0 */
  .ub-switch { inline-size: fit-content; max-inline-size: 100%; display: inline-flex; align-items: center; gap: var(--ub-space-3); font-family: var(--ub-font-ui); font-size: var(--ub-text-ui); cursor: pointer; }
  .ub-switch input {
    appearance: none; position: relative; flex: none; inline-size: 2.25rem; block-size: 1.25rem; margin: 0; min-block-size: 0;
    background: var(--ub-border-control); border-radius: var(--ub-radius-full); cursor: pointer;
    transition: background-color var(--ub-dur-base) var(--ub-ease-out);
  }
  .ub-switch input::before {
    content: ""; position: absolute; inset-block-start: 2px; inset-inline-start: 2px; inline-size: 1rem; block-size: 1rem;
    border-radius: var(--ub-radius-full); background: var(--ub-surface-raised);
    transition: translate var(--ub-dur-base) var(--ub-ease-out);
  }
  .ub-switch input:checked { background: var(--ub-accent-text); }
  .ub-switch input:checked::before { translate: 1rem 0; }
  .ub-switch input:disabled { opacity: 0.5; cursor: not-allowed; }

  /** @component segmented @summary A small set of mutually exclusive views or values. @use Two to five short options. Built from radios in a fieldset. @aliases segmented-control, toggle-group @states selected, focus-visible, disabled @since 1.0.0 */
  .ub-segmented { inline-size: fit-content; max-inline-size: 100%; display: inline-flex; flex-wrap: wrap; padding: 2px; gap: 2px; border: 1px solid var(--ub-border-control); border-radius: var(--ub-radius-control);
    background: var(--ub-surface-sunken); min-inline-size: 0; }
  .ub-segmented legend { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
  .ub-segmented label { position: relative; display: inline-flex; }
  .ub-segmented input { position: absolute; opacity: 0; inset: 0; min-block-size: 0; cursor: pointer; }
  .ub-segmented span { display: inline-flex; align-items: center; min-block-size: calc(var(--ub-control-md) - 6px); padding-inline: var(--ub-space-3);
    font-family: var(--ub-font-ui); font-size: var(--ub-text-sm); font-weight: var(--ub-weight-medium); color: var(--ub-fg-muted);
    border-radius: calc(var(--ub-radius-control) - 1px); border: 1px solid transparent; white-space: nowrap; }
  .ub-segmented input:checked + span { background: var(--ub-surface-raised); color: var(--ub-fg); border-color: var(--ub-border); }
  .ub-segmented input:focus-visible + span { outline: var(--ub-focus-width) solid var(--ub-focus); outline-offset: 1px; }
  .ub-segmented input:disabled + span { color: var(--ub-fg-faint); }

  /** @component fieldset @summary Groups related fields under a legend. @use Forms. data-layout picks stack, inline, or grid. @variants stack | inline | grid @since 1.0.0 */
  .ub-fieldset { border: 0; padding: 0; min-inline-size: 0; display: grid; gap: var(--ub-space-4); }
  .ub-fieldset > legend { font-family: var(--ub-font-ui); font-weight: var(--ub-weight-strong); font-size: var(--ub-text-ui); margin-block-end: var(--ub-space-3); padding: 0; }
  .ub-fieldset[data-layout="inline"] { display: flex; flex-wrap: wrap; gap: var(--ub-space-3) var(--ub-space-5); align-items: center; }
  .ub-fieldset[data-layout="grid"] { grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr)); }

  /* ---------- containers ---------- */
  /** @component panel @summary Groups one task's content with a title and optional actions. @use Tool screens, settings, one record. Never inside another panel or box. @aliases card, box, tile @variants default | quiet @states default, focus-within, loading @nests false @knobs --ub-panel-pad @since 1.0.0 */
  .ub-panel { background: var(--ub-surface); border: 1px solid var(--ub-border); border-radius: var(--ub-radius-surface); padding: var(--ub-panel-pad, var(--ub-space-5)); min-inline-size: 0; }
  .ub-panel[data-variant="quiet"] { background: transparent; border-width: 1px 0 0; border-radius: 0; padding-inline: 0; }
  :where([data-preset="reading"]) .ub-panel { background: transparent; border-width: 1px 0 0; border-radius: 0; padding-inline: 0; }
  .ub-panel-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--ub-space-2) var(--ub-space-4); margin-block-end: var(--ub-space-4); }
  .ub-panel-title { font-family: var(--ub-font-display); font-weight: var(--ub-weight-heading); font-size: var(--ub-text-lg); line-height: var(--ub-leading-h4); }
  .ub-panel-sub { font-size: var(--ub-text-sm); color: var(--ub-fg-muted); flex-basis: 100%; }
  .ub-panel[aria-busy="true"] { opacity: 0.6; }

  /** @component dl @summary Label and value pairs, value next to its label. @use Record details and settings summaries. Replaces two-column label and value tables. @aliases description-list, key-value @since 1.0.0 */
  .ub-dl { display: grid; align-content: start; grid-template-columns: max-content minmax(0, 1fr); gap: 0 var(--ub-space-6); font-family: var(--ub-font-ui); font-size: var(--ub-text-ui); }
  .ub-dl > dt { color: var(--ub-fg-muted); padding-block: var(--ub-space-2); border-block-end: 1px solid var(--ub-border); }
  .ub-dl > dd { padding-block: var(--ub-space-2); border-block-end: 1px solid var(--ub-border); font-variant-numeric: tabular-nums; max-inline-size: none; }
  @container (inline-size < 22rem) { .ub-dl { grid-template-columns: minmax(0, 1fr); } .ub-dl > dt { border: 0; padding-block-end: 0; } }

  /** @component callout @summary A message with a status: full border, tinted ground, icon, and a word. @use Notes, warnings, errors, and confirmations inside a page. Never a colored side stripe. @aliases alert, banner, notice @variants info | success | warning | danger @since 1.0.0 */
  .ub-callout { --_bd: var(--ub-info-border); --_bg: var(--ub-info-subtle); --_tx: var(--ub-info-text);
    display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--ub-space-1) var(--ub-space-3); align-items: start;
    padding: var(--ub-space-4); background: var(--_bg); border: 1px solid var(--_bd); border-radius: var(--ub-radius-surface);
    font-family: var(--ub-font-ui); font-size: var(--ub-text-ui); color: var(--ub-fg); }
  .ub-callout[data-tone="success"] { --_bd: var(--ub-success-border); --_bg: var(--ub-success-subtle); --_tx: var(--ub-success-text); }
  .ub-callout[data-tone="warning"] { --_bd: var(--ub-warning-border); --_bg: var(--ub-warning-subtle); --_tx: var(--ub-warning-text); }
  .ub-callout[data-tone="danger"]  { --_bd: var(--ub-danger-border);  --_bg: var(--ub-danger-subtle);  --_tx: var(--ub-danger-text); }
  .ub-callout > .ub-icon { color: var(--_tx); margin-block-start: 0.15em; }
  .ub-callout-title { font-weight: var(--ub-weight-strong); color: var(--_tx); }
  .ub-callout > :not(.ub-icon) { grid-column: 2; }
  .ub-callout p { max-inline-size: var(--ub-measure-ui); }

  /* ---------- data ---------- */
  /** @component table @summary Data table sized to its contents, headers on one line, numbers right-aligned. @use Any list of records with more than one value. Put it in table-wrap. data-sortable turns on sorting. @states default, row hover, selected, sorted, sticky header, empty, loading @since 1.0.0 */
  .ub-table { inline-size: auto; }
  .ub-table thead th { position: sticky; inset-block-start: 0; z-index: 1; background: var(--ub-bg); color: var(--ub-fg); border-block-end-color: var(--ub-border-strong); }
  .ub-table tbody th { font-weight: var(--ub-weight-medium); }
  @media (hover: hover) { .ub-table tbody tr:hover > * { background: var(--ub-surface-sunken); } }
  .ub-table tr[aria-selected="true"] > * { background: var(--ub-accent-subtle); }
  .ub-table[aria-busy="true"] tbody { opacity: 0.5; }
  .ub-table[data-density="compact"] :is(th, td), [data-preset="tool"] .ub-table :is(th, td) { padding-block: var(--ub-space-1); }
  /** @component sort @summary The button inside a sortable column header. @use Only inside th of a data-sortable table. @since 1.0.0 */
  .ub-sort { all: unset; display: inline-flex; align-items: baseline; gap: 0.35em; cursor: pointer; font: inherit; color: inherit; min-block-size: auto; }
  .ub-sort::after { content: "\2195"; color: var(--ub-fg-faint); font-size: 0.85em; }
  [aria-sort="ascending"] .ub-sort::after { content: "\2191"; color: var(--ub-fg); }
  [aria-sort="descending"] .ub-sort::after { content: "\2193"; color: var(--ub-fg); }
  .ub-sort:focus-visible { outline: var(--ub-focus-width) solid var(--ub-focus); outline-offset: 2px; }

  /** @component stat @summary One sourced figure with a label, an optional change, and its basis. @use Up to four key figures at the top of a report. A figure with %, x, or + needs a source. @aliases metric, kpi @variants default @since 1.0.0 */
  .ub-stat { display: grid; gap: var(--ub-space-1); align-content: start; font-family: var(--ub-font-ui); min-inline-size: 0; }
  .ub-stat-label { font-size: var(--ub-text-sm); color: var(--ub-fg-muted); order: -1; }
  .ub-stat-value { font-family: var(--ub-font-display); font-weight: var(--ub-weight-heading); font-size: var(--ub-text-3xl); line-height: 1.05;
    letter-spacing: var(--ub-tracking-display); font-variant-numeric: lining-nums proportional-nums; color: var(--ub-fg); margin: 0; }
  .ub-stat-delta { font-size: var(--ub-text-sm); font-weight: var(--ub-weight-medium); margin: 0; }
  .ub-stat-delta[data-direction="up"] { color: var(--ub-success-text); }
  .ub-stat-delta[data-direction="down"] { color: var(--ub-danger-text); }
  .ub-stat-delta[data-direction="up"]::before { content: "\2191 "; }
  .ub-stat-delta[data-direction="down"]::before { content: "\2193 "; }
  .ub-stat-basis { font-size: var(--ub-text-xs); color: var(--ub-fg-muted); margin: 0; }
  /** @component stats @summary A row of two to four stats divided by hairlines. @use The opening figures of a report. Never filler: every figure is real and sourced. @since 1.0.0 */
  .ub-stats { display: flex; flex-wrap: wrap; gap: var(--ub-space-6) 0; }
  .ub-stats > .ub-stat { flex: 1 1 11rem; padding-inline: var(--ub-space-5); border-inline-start: 1px solid var(--ub-border); }
  .ub-stats > .ub-stat:first-child { padding-inline-start: 0; border-inline-start: 0; }

  /** @component figure @summary A figure, diagram, or image with a caption and a required source line. @use Any number, chart, or image whose origin matters. @aliases image, diagram @since 1.0.0 */
  .ub-figure { display: grid; gap: var(--ub-space-2); align-content: start; }
  .ub-figure > figcaption, .ub-figure-caption { font-family: var(--ub-font-ui); font-size: var(--ub-text-sm); color: var(--ub-fg-muted); max-inline-size: var(--ub-measure); }
  .ub-figure-source { font-family: var(--ub-font-ui); font-size: var(--ub-text-xs); color: var(--ub-fg-muted); }

  /** @component badge @summary A short status or count. @use Status words and counts. data-tone is required. Never a decoration above a heading. @aliases tag, pill, label, chip @variants neutral | accent | info | success | warning | danger @since 1.0.0 */
  .ub-badge { --_bg: var(--ub-surface-sunken); --_fg: var(--ub-fg-muted); --_bd: var(--ub-border);
    display: inline-flex; inline-size: fit-content; align-items: center; gap: var(--ub-space-1); block-size: 1.375rem; padding-inline: var(--ub-space-2);
    font-family: var(--ub-font-ui); font-size: var(--ub-text-xs); font-weight: var(--ub-weight-medium); line-height: 1; white-space: nowrap;
    color: var(--_fg); background: var(--_bg); border: 1px solid var(--_bd); border-radius: var(--ub-radius-full); vertical-align: 0.1em; }
  .ub-badge[data-tone="accent"]  { --_bg: var(--ub-accent-subtle); --_fg: var(--ub-accent-text); --_bd: var(--ub-accent-border); }
  .ub-badge[data-tone="info"]    { --_bg: var(--ub-info-subtle); --_fg: var(--ub-info-text); --_bd: var(--ub-info-border); }
  .ub-badge[data-tone="success"] { --_bg: var(--ub-success-subtle); --_fg: var(--ub-success-text); --_bd: var(--ub-success-border); }
  .ub-badge[data-tone="warning"] { --_bg: var(--ub-warning-subtle); --_fg: var(--ub-warning-text); --_bd: var(--ub-warning-border); }
  .ub-badge[data-tone="danger"]  { --_bg: var(--ub-danger-subtle); --_fg: var(--ub-danger-text); --_bd: var(--ub-danger-border); }
  .ub-badge > .ub-icon { inline-size: 0.875rem; block-size: 0.875rem; }

  /** @component chart @summary The frame every chart sits in: title, subtitle with units, plot, legend for two or more series, source line. @use Every Chart.js or Recharts chart. Color the chart only through ub.chart.theme(). @aliases chart-frame, graph @states ready, loading, empty, error @knobs --ub-chart-h @since 1.0.0 */
  .ub-chart { display: grid; gap: var(--ub-space-3); align-content: start; min-inline-size: 0; }
  .ub-chart-head { display: grid; gap: var(--ub-space-1); }
  .ub-chart-title { font-family: var(--ub-font-ui); font-size: var(--ub-text-base); font-weight: var(--ub-weight-strong); line-height: var(--ub-leading-ui); max-inline-size: none; }
  .ub-chart-sub { font-family: var(--ub-font-ui); font-size: var(--ub-text-sm); color: var(--ub-fg-muted); }
  .ub-chart-plot { position: relative; block-size: var(--ub-chart-h, 18rem); min-inline-size: 0; }
  .ub-chart-plot > canvas { inline-size: 100% !important; block-size: 100% !important; }
  .ub-chart-legend { display: flex; flex-wrap: wrap; gap: var(--ub-space-1) var(--ub-space-4); list-style: none; padding: 0; font-family: var(--ub-font-ui); font-size: var(--ub-text-sm); color: var(--ub-fg-muted); }
  .ub-chart-legend > li { display: inline-flex; align-items: center; gap: var(--ub-space-2); margin: 0; }
  .ub-chart-legend > li::before { content: ""; inline-size: 0.75rem; block-size: 0.75rem; border-radius: 2px; background: var(--_c, var(--ub-chart-1)); }
  .ub-chart-source { font-family: var(--ub-font-ui); font-size: var(--ub-text-xs); color: var(--ub-fg-muted); }
  .ub-chart:is([data-state="loading"], [data-state="empty"], [data-state="error"]) .ub-chart-plot > canvas { visibility: hidden; }

  /** @component map @summary Frame for a Leaflet map on OpenFreeMap tiles. @use Location data. The style follows the page scheme. @knobs --ub-map-h @since 1.0.0 */
  .ub-map { display: grid; gap: var(--ub-space-2); align-content: start; }
  .ub-map-canvas { block-size: var(--ub-map-h, 22rem); border: 1px solid var(--ub-border); border-radius: var(--ub-radius-surface); overflow: hidden; background: var(--ub-surface-sunken); }

  /* ---------- overlays ---------- */
  /** @component tip @summary A short CSS tooltip from data-tip. @use Explaining an icon button or a truncated value. Never for essential information. @aliases tooltip @states hidden, shown on hover and focus-visible @since 1.0.0 */
  .ub-tip { position: relative; }
  .ub-tip::after {
    content: attr(data-tip); position: absolute; inset-block-end: calc(100% + 6px); inset-inline-start: 50%; translate: -50% 4px;
    inline-size: max-content; max-inline-size: 16rem; padding: var(--ub-space-1) var(--ub-space-2);
    font-family: var(--ub-font-ui); font-size: var(--ub-text-xs); font-weight: var(--ub-weight-medium); line-height: 1.4; white-space: normal; text-align: start;
    color: var(--ub-bg); background: var(--ub-fg); border-radius: var(--ub-radius-control);
    opacity: 0; pointer-events: none; z-index: var(--ub-z-overlay);
    transition: opacity var(--ub-dur-fast) var(--ub-ease-out), translate var(--ub-dur-fast) var(--ub-ease-out);
  }
  @media (hover: hover) { .ub-tip:hover::after { opacity: 1; translate: -50% 0; } }
  .ub-tip:focus-visible::after { opacity: 1; translate: -50% 0; }

  /** @component popover @summary Floating content opened by a button (popovertarget). @use Small forms, filters, extra details. ub.js anchors it to its button. @states closed, open @since 1.0.0 */
  .ub-popover {
    margin: 0; padding: var(--ub-space-4); max-inline-size: min(22rem, 100vw - 2rem);
    color: var(--ub-fg); background: var(--ub-surface-raised); border: 1px solid var(--ub-border); border-radius: var(--ub-radius-overlay);
    box-shadow: var(--ub-shadow-overlay); font-family: var(--ub-font-ui); font-size: var(--ub-text-ui);
  }
  @supports (position-area: block-end) {
    .ub-popover[data-anchored] { inset: auto; position-area: block-end span-inline-end; margin-block-start: 6px; position-try-fallbacks: flip-block, flip-inline; }
  }
  .ub-popover:popover-open { opacity: 1; translate: 0 0; }
  @starting-style { .ub-popover:popover-open { opacity: 0; translate: 0 -4px; } }
  .ub-popover { transition: opacity var(--ub-dur-fast) var(--ub-ease-out), translate var(--ub-dur-fast) var(--ub-ease-out), display var(--ub-dur-fast) allow-discrete, overlay var(--ub-dur-fast) allow-discrete; opacity: 0; }

  /** @component dialog @summary A modal dialog on the native dialog element. @use Confirmations and short tasks. Open with command="show-modal" commandfor="id". @aliases modal @states closed, open, entering @since 1.0.0 */
  .ub-dialog {
    inline-size: min(34rem, 100% - 2rem); max-block-size: min(90dvh, 48rem); padding: 0;
    color: var(--ub-fg); background: var(--ub-surface-overlay); border: 1px solid var(--ub-border); border-radius: var(--ub-radius-overlay);
    box-shadow: var(--ub-shadow-overlay); font-family: var(--ub-font-ui); font-size: var(--ub-text-ui);
    opacity: 0; translate: 0 6px;
    transition: opacity var(--ub-dur-base) var(--ub-ease-out), translate var(--ub-dur-base) var(--ub-ease-out), display var(--ub-dur-base) allow-discrete, overlay var(--ub-dur-base) allow-discrete;
  }
  .ub-dialog[open] { opacity: 1; translate: 0 0; }
  @starting-style { .ub-dialog[open] { opacity: 0; translate: 0 6px; } }
  .ub-dialog::backdrop { background: var(--ub-scrim); }
  .ub-dialog-head { display: flex; align-items: start; justify-content: space-between; gap: var(--ub-space-4); padding: var(--ub-space-5) var(--ub-space-5) 0; }
  .ub-dialog-title { font-family: var(--ub-font-display); font-weight: var(--ub-weight-heading); font-size: var(--ub-text-lg); line-height: var(--ub-leading-h4); }
  .ub-dialog-body { padding: var(--ub-space-3) var(--ub-space-5) var(--ub-space-5); }
  .ub-dialog-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--ub-space-2); padding: var(--ub-space-4) var(--ub-space-5); border-block-start: 1px solid var(--ub-border); }

  /** @component drawer @summary A dialog attached to one side of the screen. @use Record details and filters beside a list. data-side="start" opens on the left. @aliases sheet, tray, side-panel @states closed, open @since 1.0.0 */
  .ub-drawer {
    margin: 0 0 0 auto; inline-size: min(28rem, 100%); block-size: 100dvh; max-block-size: none; max-inline-size: 100%; padding: 0;
    color: var(--ub-fg); background: var(--ub-surface-overlay); border: 0; border-inline-start: 1px solid var(--ub-border);
    box-shadow: var(--ub-shadow-overlay); font-family: var(--ub-font-ui); font-size: var(--ub-text-ui);
    translate: 2rem 0; opacity: 0;
    transition: opacity var(--ub-dur-base) var(--ub-ease-out), translate var(--ub-dur-base) var(--ub-ease-out), display var(--ub-dur-base) allow-discrete, overlay var(--ub-dur-base) allow-discrete;
  }
  .ub-drawer[data-side="start"] { margin: 0 auto 0 0; border-inline-start: 0; border-inline-end: 1px solid var(--ub-border); translate: -2rem 0; }
  .ub-drawer[open] { translate: 0 0; opacity: 1; }
  @starting-style { .ub-drawer[open] { translate: 2rem 0; opacity: 0; } .ub-drawer[data-side="start"][open] { translate: -2rem 0; } }
  .ub-drawer::backdrop { background: var(--ub-scrim); }

  /** @component toast @summary A short confirmation that disappears on its own. @use After an action completes. Created with ub.toast(text, {tone}). @aliases snackbar, notification @states entering, visible, exiting @since 1.0.0 */
  .ub-toast-region { position: fixed; inset: auto var(--ub-space-4) var(--ub-space-4) auto; margin: 0; padding: 0; border: 0; background: transparent;
    display: grid; gap: var(--ub-space-2); justify-items: end; z-index: var(--ub-z-toast); overflow: visible; }
  .ub-toast { display: flex; align-items: center; gap: var(--ub-space-3); max-inline-size: min(26rem, 100vw - 2rem); padding: var(--ub-space-3) var(--ub-space-4);
    color: var(--ub-fg); background: var(--ub-surface-raised); border: 1px solid var(--ub-border); border-radius: var(--ub-radius-overlay);
    box-shadow: var(--ub-shadow-overlay); font-family: var(--ub-font-ui); font-size: var(--ub-text-ui);
    transition: opacity var(--ub-dur-base) var(--ub-ease-out), translate var(--ub-dur-base) var(--ub-ease-out); }
  @starting-style { .ub-toast { opacity: 0; translate: 0 8px; } }
  .ub-toast[data-leaving] { opacity: 0; translate: 0 8px; }
  .ub-toast > .ub-icon { flex: none; }
  .ub-toast[data-tone="success"] > .ub-icon { color: var(--ub-success-text); }
  .ub-toast[data-tone="danger"] > .ub-icon { color: var(--ub-danger-text); }
  .ub-toast[data-tone="warning"] > .ub-icon { color: var(--ub-warning-text); }
  .ub-toast[data-tone="info"] > .ub-icon { color: var(--ub-info-text); }

  /* ---------- disclosure ---------- */
  /** @component tabs @summary Peer views of one dataset. @use Only when the views are peers (week, month, year). Content that should be read in order stays on the page, not behind tabs. @states selected, hover, focus-visible, disabled @parts ub-tablist ub-tab @since 1.0.0 */
  .ub-tabs { display: grid; gap: var(--ub-space-4); align-content: start; }
  .ub-tablist { display: flex; gap: var(--ub-space-1); border-block-end: 1px solid var(--ub-border); overflow-x: auto; scrollbar-width: none; }
  .ub-tab { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; min-block-size: var(--ub-control-md); padding-inline: var(--ub-space-3);
    font-family: var(--ub-font-ui); font-size: var(--ub-text-ui); font-weight: var(--ub-weight-medium); color: var(--ub-fg-muted); white-space: nowrap; cursor: pointer;
    box-shadow: inset 0 -2px 0 transparent; transition: color var(--ub-dur-fast) var(--ub-ease-out), box-shadow var(--ub-dur-fast) var(--ub-ease-out); }
  @media (hover: hover) { .ub-tab:hover { color: var(--ub-fg); } }
  .ub-tab[aria-selected="true"] { color: var(--ub-fg); box-shadow: inset 0 -2px 0 var(--ub-accent-text); }
  .ub-tab:focus-visible { outline: var(--ub-focus-width) solid var(--ub-focus); outline-offset: -2px; }
  .ub-tab:disabled, .ub-tab[aria-disabled="true"] { color: var(--ub-fg-faint); cursor: not-allowed; }

  /** @component accordion @summary Sections that open one at a time, on details name. @use Secondary detail and FAQs that are genuinely optional reading. @states closed, open, focus-visible @since 1.0.0 */
  .ub-accordion { border-block-start: 1px solid var(--ub-border); }
  .ub-accordion > details { border-block-end: 1px solid var(--ub-border); }
  .ub-accordion summary { display: flex; justify-content: space-between; gap: var(--ub-space-4); padding-block: var(--ub-space-3); cursor: pointer; list-style: none;
    font-family: var(--ub-font-ui); font-size: var(--ub-text-ui); font-weight: var(--ub-weight-medium); }
  .ub-accordion summary::-webkit-details-marker { display: none; }
  .ub-accordion summary::after { content: "+"; color: var(--ub-fg-muted); font-weight: var(--ub-weight-body); }
  .ub-accordion details[open] > summary::after { content: "\2212"; }
  .ub-accordion details > :not(summary) { padding-block-end: var(--ub-space-4); }

  /* ---------- feedback ---------- */
  /** @component empty @summary What is missing, what to do, and one button. @use Every list, table, and chart that can be empty. No illustration. @aliases empty-state, zero-state @since 1.0.0 */
  .ub-empty { display: grid; gap: var(--ub-space-2); justify-items: start; align-content: start; padding-block: var(--ub-space-8); font-family: var(--ub-font-ui); }
  .ub-empty-title { font-weight: var(--ub-weight-strong); font-size: var(--ub-text-ui); color: var(--ub-fg); }
  .ub-empty p:not(.ub-empty-title) { color: var(--ub-fg-muted); font-size: var(--ub-text-sm); max-inline-size: var(--ub-measure-ui); }
  .ub-empty > .ub-button { margin-block-start: var(--ub-space-2); }

  /** @component skeleton @summary A static placeholder block while content loads. No shimmer. @use Holding layout for content that loads after the page. @knobs --ub-skeleton-w --ub-skeleton-h @since 1.0.0 */
  .ub-skeleton { display: block; inline-size: var(--ub-skeleton-w, 100%); block-size: var(--ub-skeleton-h, 1em); background: var(--ub-surface-sunken); border-radius: var(--ub-radius-control); }

  /** @component spinner @summary The only infinite animation in the system. @use Inside a busy button or region, with role="status" and visually hidden text. @variants sm | md @since 1.0.0 */
  .ub-spinner { display: inline-block; inline-size: 1em; block-size: 1em; flex: none; border: 2px solid var(--ub-border-strong); border-block-start-color: currentColor;
    border-radius: var(--ub-radius-full); animation: ub-spin 700ms linear infinite; vertical-align: -0.15em; }
  .ub-spinner[data-size="sm"] { inline-size: 0.875em; block-size: 0.875em; }
  @media (prefers-reduced-motion: reduce) { .ub-spinner { animation-duration: 2.4s; } }

  /** @component progress @summary Determinate or indeterminate progress, or a meter. @use Uploads, steps of a job, a level against a range. @states determinate, indeterminate, complete @since 1.0.0 */
  .ub-progress { appearance: none; display: block; inline-size: 100%; block-size: 0.5rem; border: 1px solid var(--ub-border-strong); border-radius: var(--ub-radius-full); background: var(--ub-surface-raised); overflow: hidden; color: var(--ub-accent-text); }
  .ub-progress::-webkit-progress-bar { background: var(--ub-surface-raised); }
  .ub-progress::-webkit-progress-value { background: var(--ub-accent-text); border-radius: var(--ub-radius-full); }
  .ub-progress::-moz-progress-bar { background: var(--ub-accent-text); border-radius: var(--ub-radius-full); }
  .ub-progress::-webkit-meter-bar { background: var(--ub-surface-raised); border: 0; border-radius: var(--ub-radius-full); }
  .ub-progress::-webkit-meter-optimum-value { background: var(--ub-success); }
  .ub-progress::-webkit-meter-suboptimum-value { background: var(--ub-warning); }
  .ub-progress::-webkit-meter-even-less-good-value { background: var(--ub-danger); }

  /* ---------- navigation ---------- */
  /** @component appbar @summary The full-width bar at the top of a page or app. @use Product name or mark, primary navigation, one or two actions. Never the system wordmark beside a title. @aliases header, navbar, topbar @since 1.0.0 */
  .ub-appbar { position: sticky; inset-block-start: 0; z-index: var(--ub-z-sticky); background: var(--ub-bg); border-block-end: 1px solid var(--ub-border); }
  .ub-appbar-inner { display: flex; align-items: center; gap: var(--ub-space-4); min-block-size: 3.5rem; flex-wrap: wrap; padding-block: var(--ub-space-2); }
  .ub-appbar-brand { display: inline-flex; align-items: center; gap: var(--ub-space-2); color: var(--ub-fg); text-decoration: none;
    font-family: var(--ub-font-display); font-weight: var(--ub-weight-heading); font-size: var(--ub-text-base); }
  .ub-appbar-end { margin-inline-start: auto; display: flex; flex-wrap: wrap; align-items: center; gap: var(--ub-space-3); }

  /** @component nav @summary A list of page links. @use Top navigation in an appbar. aria-current="page" marks the current page. @states current page, hover, focus-visible @since 1.0.0 */
  .ub-nav ul { display: flex; flex-wrap: wrap; gap: var(--ub-space-1) var(--ub-space-4); list-style: none; padding: 0; }
  .ub-nav li { margin: 0; }
  .ub-nav a { display: inline-flex; align-items: center; min-block-size: var(--ub-control-sm); font-family: var(--ub-font-ui); font-size: var(--ub-text-ui);
    color: var(--ub-fg-muted); text-decoration: none; }
  @media (hover: hover) { .ub-nav a:hover { color: var(--ub-fg); } }
  .ub-nav a[aria-current="page"] { color: var(--ub-fg); text-decoration: underline; text-decoration-thickness: 2px; text-decoration-color: var(--ub-accent-text); text-underline-offset: 0.4em; }

  /** @component sidenav @summary A vertical list of sections or pages. @use App sidebars and long-page indexes. @states current, hover, focus-visible @since 1.0.0 */
  .ub-sidenav ul { list-style: none; padding: 0; display: grid; gap: 1px; }
  .ub-sidenav li { margin: 0; }
  .ub-sidenav a { display: flex; align-items: center; gap: var(--ub-space-2); min-block-size: var(--ub-control-sm); padding-inline: var(--ub-space-3);
    border-radius: var(--ub-radius-control); font-family: var(--ub-font-ui); font-size: var(--ub-text-sm); color: var(--ub-fg-muted); text-decoration: none; }
  @media (hover: hover) { .ub-sidenav a:hover { color: var(--ub-fg); background: var(--ub-surface-sunken); } }
  .ub-sidenav a[aria-current] { color: var(--ub-fg); background: var(--ub-surface-sunken); font-weight: var(--ub-weight-strong); }
  .ub-sidenav-label { font-family: var(--ub-font-ui); font-size: var(--ub-text-xs); font-weight: var(--ub-weight-strong); color: var(--ub-fg-muted);
    padding: var(--ub-space-4) var(--ub-space-3) var(--ub-space-1); }

  /** @component app @summary App shell: appbar on top, sidebar beside main. @use Tool pages. Below 64rem the sidebar stacks above main. @since 1.0.0 */
  .ub-app { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto 1fr; min-block-size: 100dvh; }
  .ub-app > .ub-appbar { grid-column: 1 / -1; }
  .ub-app-side { background: var(--ub-surface); border-block-end: 1px solid var(--ub-border); padding: var(--ub-space-3); }
  .ub-app-main { min-inline-size: 0; padding: var(--ub-space-6) max(var(--ub-gutter), env(safe-area-inset-right)) var(--ub-space-12) max(var(--ub-gutter), env(safe-area-inset-left)); }
  @media (width >= 64rem) {
    .ub-app { grid-template-columns: 15rem minmax(0, 1fr); grid-template-rows: auto 1fr; }
    .ub-app-side { border-block-end: 0; border-inline-end: 1px solid var(--ub-border); position: sticky; inset-block-start: 3.5rem; block-size: calc(100dvh - 3.5rem); overflow-y: auto; }
  }

  /** @component toolbar @summary A row of controls above a table or view. @use Filters, search, and view switches. @since 1.0.0 */
  .ub-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ub-space-2) var(--ub-space-3); padding-block: var(--ub-space-3); }
  .ub-toolbar-end { margin-inline-start: auto; display: flex; flex-wrap: wrap; gap: var(--ub-space-2); }

  /** @component code @summary A code block with a copy button. @use Snippets someone will copy. @states default, copied @since 1.0.0 */
  .ub-code { position: relative; }
  .ub-code > pre { overflow: auto; padding: var(--ub-space-4); padding-inline-end: 5rem; background: var(--ub-surface-sunken); border: 1px solid var(--ub-border);
    border-radius: var(--ub-radius-surface); font-size: var(--ub-text-sm); line-height: 1.6; tab-size: 2; }
  .ub-code-copy { position: absolute; inset-block-start: var(--ub-space-2); inset-inline-end: var(--ub-space-2); }

  /** @component skip @summary Skip link to the main content. @use First focusable element on every page. @since 1.0.0 */
  .ub-skip { position: absolute; inset-inline-start: var(--ub-space-2); inset-block-start: -10rem; z-index: var(--ub-z-toast);
    padding: var(--ub-space-2) var(--ub-space-3); background: var(--ub-surface-raised); color: var(--ub-fg); border: 1px solid var(--ub-border-strong); border-radius: var(--ub-radius-control); }
  .ub-skip:focus-visible { inset-block-start: var(--ub-space-2); }
}

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

/* ============================== utilities ============================== */
@layer ub.utilities {
  /** @component num @summary Right-aligned tabular figure. @use Number cells and inline figures. @since 1.0.0 */
  .ub-num { text-align: end; font-variant-numeric: tabular-nums; }
  /** @component nowrap @summary Keeps a value on one line. @since 1.0.0 */
  .ub-nowrap { white-space: nowrap; }
  /** @component sr-only @summary Visually hidden, still read by screen readers. @aliases visually-hidden @since 1.0.0 */
  .ub-sr-only { position: absolute !important; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  /** @component muted @summary Secondary text color. @use Captions and metadata. Never on body text. @since 1.0.0 */
  .ub-muted { color: var(--ub-fg-muted); }
}
