/* Design tokens — RESOLVED at request time from the substrate floor
   plus this tenant's activated Design System Profile
   (ubos_ui_renderer::profile::compile_resolved_css over
   design_system::resolve_profile). Not a checked-in snapshot: change
   the tenant's active profile and this block changes with it. */
:root {
  --uir-bg: #f8fafc;
  --uir-card: #ffffff;
  --uir-fg: #0f172a;
  --uir-muted: #64748b;
  --uir-border: #e2e8f0;
  --uir-primary: #1d4ed8;
  --uir-primary-hover: #1e40af;
  --uir-on-primary: #ffffff;
  --uir-danger: #b91c1c;
  --uir-ok-bg: #f0fdf4;
  --uir-ok-fg: #166534;
  --uir-warn-bg: #fffbeb;
  --uir-warn-fg: #92400e;
  --uir-focus-ring: #1d4ed8;
  --uir-space-xs: 4px;
  --uir-space-sm: 8px;
  --uir-space-md: 16px;
  --uir-space: 16px;
  --uir-space-lg: 24px;
  --uir-space-xl: 40px;
  --uir-radius-none: 0px;
  --uir-radius-sm: 6px;
  --uir-radius-md: 10px;
  --uir-radius: 10px;
  --uir-radius-lg: 16px;
  --uir-radius-full: 9999px;
  --uir-type-body-size: 16px;
  --uir-type-body-weight: 400;
  --uir-font: Inter, system-ui, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif;
  --uir-type-heading-size: 26px;
  --uir-type-heading-weight: 700;
  --uir-type-caption-size: 0.875rem;
  --uir-type-caption-weight: 400;
  --uir-type-caption-line: 1.5;
  --uir-density-scale: 1;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --uir-bg: color-mix(in srgb, #f8fafc 14%, #10161f);
    --uir-card: color-mix(in srgb, #ffffff 14%, #10161f);
    --uir-fg: color-mix(in srgb, #0f172a 22%, #e8eef5);
    --uir-muted: color-mix(in srgb, #64748b 22%, #e8eef5);
    --uir-border: color-mix(in srgb, #e2e8f0 14%, #10161f);
    --uir-primary: #1d4ed8;
    --uir-primary-hover: #1e40af;
    --uir-on-primary: #ffffff;
    --uir-danger: #b91c1c;
    --uir-ok-bg: color-mix(in srgb, #f0fdf4 14%, #10161f);
    --uir-ok-fg: color-mix(in srgb, #166534 22%, #e8eef5);
    --uir-warn-bg: color-mix(in srgb, #fffbeb 14%, #10161f);
    --uir-warn-fg: color-mix(in srgb, #92400e 22%, #e8eef5);
    --uir-focus-ring: #1d4ed8;
  }
}
:root[data-theme="dark"] {
    --uir-bg: color-mix(in srgb, #f8fafc 14%, #10161f);
    --uir-card: color-mix(in srgb, #ffffff 14%, #10161f);
    --uir-fg: color-mix(in srgb, #0f172a 22%, #e8eef5);
    --uir-muted: color-mix(in srgb, #64748b 22%, #e8eef5);
    --uir-border: color-mix(in srgb, #e2e8f0 14%, #10161f);
    --uir-primary: #1d4ed8;
    --uir-primary-hover: #1e40af;
    --uir-on-primary: #ffffff;
    --uir-danger: #b91c1c;
    --uir-ok-bg: color-mix(in srgb, #f0fdf4 14%, #10161f);
    --uir-ok-fg: color-mix(in srgb, #166534 22%, #e8eef5);
    --uir-warn-bg: color-mix(in srgb, #fffbeb 14%, #10161f);
    --uir-warn-fg: color-mix(in srgb, #92400e 22%, #e8eef5);
    --uir-focus-ring: #1d4ed8;
}
:focus-visible {
  outline: 2px solid var(--uir-focus-ring);
  outline-offset: 2px;
}
/* Component-role variant hooks — one class per role, carrying the
   density/emphasis this profile resolved for it. `uir.css` consumes
   --uir-role-pad / --uir-role-weight; the two symbolic values are
   carried so an adapter can branch on the choice itself. */
.uir-action-primary {
  --uir-role-density: comfortable;
  --uir-role-emphasis: high;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 650;
}
.uir-action-row {
  --uir-role-density: compact;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-xs);
  --uir-role-weight: 400;
}
.uir-action-secondary {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-action-standalone {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-action-row_standalone {
  --uir-role-density: compact;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-xs);
  --uir-role-weight: 400;
}
.uir-badge {
  --uir-role-density: compact;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-xs);
  --uir-role-weight: 400;
}
.uir-bounded_grid {
  --uir-role-density: compact;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-xs);
  --uir-role-weight: 400;
}
.uir-card_collection {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-chart {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-detail {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-feedback-empty {
  --uir-role-density: comfortable;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 400;
}
.uir-feedback-error {
  --uir-role-density: comfortable;
  --uir-role-emphasis: high;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 650;
}
.uir-feedback-loading {
  --uir-role-density: comfortable;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 400;
}
.uir-feedback-receipt {
  --uir-role-density: comfortable;
  --uir-role-emphasis: high;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 650;
}
.uir-feedback-refusal {
  --uir-role-density: comfortable;
  --uir-role-emphasis: high;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 650;
}
.uir-feedback-confirmation {
  --uir-role-density: comfortable;
  --uir-role-emphasis: high;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 650;
}
.uir-feedback-conflict {
  --uir-role-density: comfortable;
  --uir-role-emphasis: high;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 650;
}
.uir-feedback-stale {
  --uir-role-density: comfortable;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 400;
}
.uir-feedback-retry {
  --uir-role-density: comfortable;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 400;
}
.uir-feedback-offline {
  --uir-role-density: comfortable;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 400;
}
.uir-feedback-pending {
  --uir-role-density: comfortable;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 400;
}
.uir-feedback-failure {
  --uir-role-density: comfortable;
  --uir-role-emphasis: high;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 650;
}
.uir-feedback-toast {
  --uir-role-density: comfortable;
  --uir-role-emphasis: high;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 650;
}
.uir-workspace-detail {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-workspace-queue {
  --uir-role-density: compact;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-xs);
  --uir-role-weight: 400;
}
.uir-workspace-context {
  --uir-role-density: compact;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-xs);
  --uir-role-weight: 400;
}
.uir-form {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-grouped_fields {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-image {
  --uir-role-density: comfortable;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 400;
}
.uir-list {
  --uir-role-density: compact;
  --uir-role-emphasis: low;
  --uir-role-pad: var(--uir-space-xs);
  --uir-role-weight: 400;
}
.uir-stat {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-stepper {
  --uir-role-density: comfortable;
  --uir-role-emphasis: medium;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 500;
}
.uir-hero {
  --uir-role-density: comfortable;
  --uir-role-emphasis: high;
  --uir-role-pad: var(--uir-space-sm);
  --uir-role-weight: 650;
}
/* Design-package rhythm tokens — the spacing, radius, elevation and
   font scales THIS host's design package states
   (ubos_ui_renderer::profile::compile_package_css). Appended after the
   resolved block above, so a package that states a step wins by cascade
   order and one that states none changes nothing. Colour is deliberately
   absent — it has the resolver ladder. */
:root {
  --uir-bg: #F5F8FA;
  --uir-card: #FFFFFF;
  --uir-fg: #13202E;
  --uir-muted: #5B6B7A;
  --uir-border: #E3E9EF;
  --uir-primary: #0B2545;
  --uir-primary-hover: #061A33;
  --uir-on-primary: #FFFFFF;
  --uir-danger: #C43D3D;
  --uir-accent: #13C4D6;
  --uir-ok-bg: #E9F6EF;
  --uir-ok-fg: #1E8E5A;
  --uir-warn-bg: #FBECEC;
  --uir-warn-fg: #C43D3D;
  --uir-focus-ring: #13C4D6;
  --uir-rail-bg: #0B2545;
  --uir-rail-active: #13C4D6;
  --uir-rail-fg: #F5F8FA;
  --uir-rail-muted: #A9BED2;
  --uir-space-lg: 24px;
  --uir-space-md: 16px;
  --uir-space: 16px;
  --uir-space-sm: 8px;
  --uir-space-xl: 32px;
  --uir-space-xs: 4px;
  --uir-radius-full: 999px;
  --uir-radius-lg: 10px;
  --uir-radius-md: 10px;
  --uir-radius: 10px;
  --uir-radius-none: 0;
  --uir-radius-sm: 8px;
  --uir-shadow-lg: 0 6px 16px -8px rgb(11 37 69 / 18%);
  --uir-shadow-md: 0 6px 16px -8px rgb(11 37 69 / 18%);
  --uir-shadow-sm: 0 6px 16px -8px rgb(11 37 69 / 18%);
  --uir-font: Public Sans, system-ui, sans-serif;
}

/* Adapter styling for the Render IR — the hand-authored half.
 *
 * The design tokens this file consumes are NOT here: they are compiled
 * per request by `apps::asset_css` from the substrate floor plus the
 * tenant's activated Design System Profile, and prepended above these
 * rules. That is deliberate — every colour, radius, space and type step
 * below is `var(--uir-*)`, so a tenant who activates a different
 * profile re-themes every generated app without this file changing.
 *
 * House rules for editing:
 *  - No literal colours. If you need one, it is a missing token.
 *  - Nothing here may make an element MEAN something different; this
 *    file styles the semantic elements the renderer already chose.
 *  - Vernacular first: the four-locale floor means Devanagari, Kannada
 *    and Tamil set alongside Latin, so line-heights are generous and
 *    nothing is locked to a Latin cap-height.
 */

/* ---------------------------------------------------------------
 * Adapter-local scales.
 *
 * Elevation and motion are presentation of the ADAPTER, not substrate
 * tokens — a SwiftUI adapter would express both natively and share
 * nothing with these. Tinted rather than neutral-black shadows: a flat
 * rgba(0,0,0,…) over a warm surface reads as grey dirt, which is most
 * of what makes generated UI look cheap.
 * --------------------------------------------------------------- */
:root {
  --uir-shadow-sm: 0 1px 2px rgb(16 24 40 / 6%), 0 1px 3px rgb(16 24 40 / 4%);
  --uir-shadow-md: 0 2px 4px rgb(16 24 40 / 5%), 0 6px 16px -4px rgb(16 24 40 / 10%);
  --uir-shadow-lg: 0 12px 32px -8px rgb(16 24 40 / 18%);
  --uir-ease: cubic-bezier(.2, .7, .3, 1);
  --uir-fast: 120ms;
  --uir-slow: 220ms;
  /* Shell metrics are resolved from the Design Package at runtime; these
   * fallbacks keep the adapter valid before the first Render IR arrives. */
  --uir-sidebar-width: 18rem;
  --uir-content-inset: var(--uir-space-lg);
  --uir-shell-inset: var(--uir-space-md);
  --uir-item-gap: var(--uir-space-sm);
  --uir-corner-radius: var(--uir-radius-lg);
  /* Row/hover tints derive from the theme's own ink so they survive a
   * profile swap: no separate "hover grey" token to keep in sync. */
  --uir-tint: color-mix(in srgb, var(--uir-fg) 4%, transparent);
  --uir-tint-strong: color-mix(in srgb, var(--uir-fg) 7%, transparent);
  /* The media scrim, and the ink that survives it (Phase 12 Wave A's
   * `overlay` card preset). Deliberately NOT derived from the theme
   * and deliberately not a substrate token: text sitting over an
   * arbitrary tenant photograph has no contrast guarantee at all, and
   * the only thing that restores one is a scrim dark enough that
   * light ink reads over ANY image, in either theme. A theme-derived
   * scrim would invert with the theme and lose exactly that. This is
   * adapter presentation, same class as the shadow scale above. */
  --uir-scrim: linear-gradient(to top, rgb(16 24 40 / 78%), rgb(16 24 40 / 12%));
  --uir-on-scrim: #fff;
  --uir-accent-tint: color-mix(in srgb, var(--uir-primary) 8%, transparent);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --uir-shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
    --uir-shadow-md: 0 2px 6px rgb(0 0 0 / 45%), 0 8px 24px -6px rgb(0 0 0 / 50%);
    --uir-shadow-lg: 0 16px 40px -12px rgb(0 0 0 / 60%);
  }
}
:root[data-theme="dark"] {
  --uir-shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
  --uir-shadow-md: 0 2px 6px rgb(0 0 0 / 45%), 0 8px 24px -6px rgb(0 0 0 / 50%);
  --uir-shadow-lg: 0 16px 40px -12px rgb(0 0 0 / 60%);
}

/* The app root. Type comes from the resolved profile, never from the
 * host page — a generated app must look the same wherever it is
 * embedded. */
#app {
  font-family: var(--uir-font);
  font-size: var(--uir-type-body-size);
  line-height: var(--uir-type-body-line);
  color: var(--uir-fg);
  /* Numerals line up in every table, stat and money field. Business
   * software is mostly columns of numbers being compared. */
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------
 * Navigation — screen tabs.
 * --------------------------------------------------------------- */
.uir-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--uir-space-xs);
  margin-bottom: var(--uir-space-lg);
  border-bottom: 1px solid var(--uir-border);
}
.uir-tab {
  appearance: none;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: var(--uir-space-sm) var(--uir-space-md);
  font: inherit;
  font-weight: 500;
  color: var(--uir-muted);
  border-radius: var(--uir-radius-sm) var(--uir-radius-sm) 0 0;
  cursor: pointer;
  transition: color var(--uir-fast) var(--uir-ease),
              background var(--uir-fast) var(--uir-ease);
}
.uir-tab:hover { color: var(--uir-fg); background: var(--uir-tint); }
.uir-tab.active {
  color: var(--uir-primary);
  border-bottom-color: var(--uir-primary);
  font-weight: 600;
}

/* ---------------------------------------------------------------
 * Surfaces.
 * --------------------------------------------------------------- */
.uir-card {
  background: var(--uir-card);
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-lg);
  box-shadow: var(--uir-shadow-sm);
  padding: var(--uir-space-lg);
  margin-bottom: var(--uir-space-md);
}
.uir-card > h2 {
  margin: 0 0 var(--uir-space-md);
  font-size: var(--uir-type-heading-size);
  font-weight: var(--uir-type-heading-weight);
  line-height: var(--uir-type-heading-line);
  letter-spacing: -0.01em;
}

/* A section is a heading + lede: page furniture, not a card. */
.uir-section {
  margin: var(--uir-space-xl) 0 var(--uir-space-md);
}
.uir-section:first-child { margin-top: 0; }
.uir-section h2 {
  margin: 0 0 var(--uir-space-xs);
  font-size: calc(var(--uir-type-heading-size) * 1.15);
  font-weight: var(--uir-type-heading-weight);
  line-height: var(--uir-type-heading-line);
  letter-spacing: -0.02em;
}
.uir-section p { margin: 0; color: var(--uir-muted); max-width: 62ch; }

.uir-heading {
  font-size: var(--uir-type-heading-size);
  font-weight: var(--uir-type-heading-weight);
  line-height: var(--uir-type-heading-line);
}
.uir-body { color: var(--uir-fg); max-width: 68ch; }
.uir-muted { color: var(--uir-muted); }

/* ---------------------------------------------------------------
 * Data — the surface a business actually stares at.
 *
 * Sticky header, hairline rows, no zebra (zebra fights the tinted
 * hover and adds noise at the density these tables run at), numeric
 * columns right-aligned by the adapter's own alignment class.
 * --------------------------------------------------------------- */
.uir-list, .uir-bounded_grid {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--uir-type-body-size);
}
.uir-list th, .uir-bounded_grid th {
  position: sticky;
  top: 0;
  z-index: 1;
  text-align: left;
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--uir-muted);
  background: var(--uir-card);
  border-bottom: 1px solid var(--uir-border);
  padding: var(--uir-space-sm) var(--uir-space-sm);
  white-space: nowrap;
}
.uir-list td, .uir-bounded_grid td {
  border-bottom: 1px solid var(--uir-border);
  padding: var(--uir-space-sm);
  vertical-align: baseline;
}
.uir-list tbody tr, .uir-bounded_grid tbody tr {
  transition: background var(--uir-fast) var(--uir-ease);
}
.uir-list tbody tr:hover, .uir-bounded_grid tbody tr:hover {
  background: var(--uir-tint);
}
.uir-list tbody tr:last-child td,
.uir-bounded_grid tbody tr:last-child td { border-bottom: 0; }

/* A wide table scrolls inside its own card rather than pushing the
 * page sideways — the one thing that breaks a phone layout outright. */
.uir-card:has(> .uir-list),
.uir-card:has(> .uir-bounded_grid) { overflow-x: auto; }

/* ---------------------------------------------------------------
 * Cards-of-records (card_collection, and the adapter's card variant
 * of a list). Auto-fit so one record does not stretch to full width.
 * --------------------------------------------------------------- */
.uir-card_collection, .uir-list-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
  gap: var(--uir-space-md);
}
.uir-mini-card {
  background: var(--uir-card);
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-md);
  /* The component-role variant hook. `--uir-role-pad` is emitted per
   * component_role by `ubos_ui_renderer::profile`'s
   * `component_variant_hooks` onto the ROLE class this card sits
   * inside (`.uir-card_collection` / `.uir-list-cards`), and custom
   * properties inherit — so a density resolved for the collection
   * reaches its cards without the adapter passing anything down. The
   * fallback keeps every pre-Phase-12 stylesheet rendering unchanged. */
  padding: var(--uir-role-pad, var(--uir-space-md));
  display: flex;
  flex-direction: column;
  gap: var(--uir-space-xs);
  transition: box-shadow var(--uir-fast) var(--uir-ease),
              transform var(--uir-fast) var(--uir-ease),
              border-color var(--uir-fast) var(--uir-ease);
}
/* Lift only when the card is actually actionable. */
.uir-mini-card:has(.uir-card-actions button):hover {
  box-shadow: var(--uir-shadow-md);
  border-color: color-mix(in srgb, var(--uir-primary) 35%, var(--uir-border));
  transform: translateY(-1px);
}
/* The card's name — the one thing a customer reads first. */
.uir-card-title {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--uir-fg);
}
.uir-kv { font-size: .9375rem; color: var(--uir-fg); }
.uir-kv-label { color: var(--uir-muted); }
.uir-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--uir-space-xs);
  margin-top: auto;
  padding-top: var(--uir-space-sm);
}

/* ---------------------------------------------------------------
 * Card arrangement presets (Phase 12 Wave A).
 *
 * Nine named points in the arrangement grammar of
 * docs/superpowers/specs/2026-08-17-card-variants-and-design-packages-design.md
 * §4.1. The renderer passes the authored preset tag through to the IR
 * unchanged; `uir-adapter.js` puts `uir-preset-<tag>` on the
 * collection BESIDE its role class, so every rule above still applies
 * and a preset can only add to it. A card wearing no preset renders
 * exactly as it did before this phase.
 *
 * WHAT THESE RULES CAN AND CANNOT SAY. The mini-card DOM today is
 * [picture?] title, key/value rows, actions — it has no SLOTS. So the
 * arrangement properties that place a slot (`figure: dominant`,
 * `items: rows`, `omit: [supporting]`) are expressed here only as far
 * as position-independent CSS can reach: a preset that wants the
 * MONEY field larger can only enlarge every value, because nothing in
 * this stylesheet knows which value is the money one. Slot inference
 * is the design's §3, and Phase 12 Wave B owns it — at which point
 * these rules get more precise WITHOUT the preset names changing,
 * which is the point of naming the preset rather than the CSS.
 *
 * Kept honest deliberately: no rule below hides a field a tenant
 * declared unless the preset's own grammar row says to omit that slot
 * (`tile` omits supporting + action; `note` omits the figure), because
 * design §6.1's dangerous case is the SAFE-LOOKING default that drops
 * the one thing the card existed to show.
 * --------------------------------------------------------------- */

/* menu — media top, figure inline. The catalogue card, and the shape
 * the default already has; stated explicitly so "menu" is a choice
 * rather than the absence of one. */
.uir-preset-menu .uir-mini-card { text-align: start; }

/* overlay — media behind, align start. A featured item: the picture
 * fills the card and the text sits over a scrim. The scrim is not
 * decoration — text over an arbitrary photograph has no contrast
 * guarantee without it. */
.uir-preset-overlay .uir-mini-card {
  position: relative;
  min-height: 12rem;
  justify-content: flex-end;
  isolation: isolate;
  overflow: hidden;
}
.uir-preset-overlay .uir-card-image {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  margin: 0;
  border-radius: inherit;
  aspect-ratio: auto;
}
.uir-preset-overlay .uir-mini-card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--uir-scrim);
}
.uir-preset-overlay .uir-card-title,
.uir-preset-overlay .uir-kv,
.uir-preset-overlay .uir-kv-label { color: var(--uir-on-scrim); }

/* media_side — media left, figure inline. Order lines, appointments:
 * a thumbnail beside the text rather than above it. */
.uir-preset-media_side .uir-mini-card {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  grid-template-rows: auto;
  column-gap: var(--uir-space-md);
  align-items: start;
}
.uir-preset-media_side .uir-card-image {
  grid-row: 1 / -1;
  margin: 0;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--uir-radius-sm);
}
.uir-preset-media_side .uir-mini-card > :not(.uir-card-image) { grid-column: 2; }

/* profile — media left + circle, align center. A person. */
.uir-preset-profile .uir-mini-card {
  align-items: center;
  text-align: center;
}
.uir-preset-profile .uir-card-image {
  margin: 0 0 var(--uir-space-sm);
  width: 4.5rem;
  aspect-ratio: 1 / 1;
  border-radius: 50%;
}
.uir-preset-profile .uir-card-actions { justify-content: center; }

/* pricing — no media, figure dominant, align center. Tiers and price
 * lists: the number is the point, so the values lead and the labels
 * recede. Slot-blind for now (see the header note): every value grows,
 * not just the money one. */
.uir-preset-pricing .uir-card-image { display: none; }
.uir-preset-pricing .uir-mini-card {
  text-align: center;
  align-items: center;
  padding-block: var(--uir-space-lg);
}
.uir-preset-pricing .uir-kv { font-size: 1.5rem; font-weight: 600; }
.uir-preset-pricing .uir-kv-label {
  display: block;
  font-size: .8125rem;
  font-weight: 400;
}
.uir-preset-pricing .uir-card-actions { justify-content: center; }

/* tile — no media, figure dominant, supporting + action omitted. The
 * KPI strip: the grammar's `omit: [supporting, action]` row is why the
 * actions are hidden here and nowhere else. */
.uir-preset-tile .uir-card-image,
.uir-preset-tile .uir-card-actions { display: none; }
.uir-preset-tile .uir-mini-card {
  gap: 0;
  padding: var(--uir-role-pad, var(--uir-space-sm));
}
.uir-preset-tile .uir-card-title {
  font-size: .8125rem;
  font-weight: 500;
  color: var(--uir-muted);
  order: 2;
}
.uir-preset-tile .uir-kv { font-size: 1.75rem; font-weight: 650; order: 1; }
.uir-preset-tile .uir-kv-label { display: none; }

/* ledger — no media, figure prominent + trailing. Invoice lines and
 * totals: label left, value right, figures aligned. `items: rows`
 * (nested line items) needs the `items` slot and lands with Wave B. */
.uir-preset-ledger .uir-card-image { display: none; }
.uir-preset-ledger .uir-kv {
  display: flex;
  justify-content: space-between;
  gap: var(--uir-space-md);
  font-variant-numeric: tabular-nums;
  border-bottom: 1px solid var(--uir-border);
  padding-block: .2rem;
}
.uir-preset-ledger .uir-kv > :last-child { font-weight: 600; }

/* compact — no media, density compact. History and queues: more rows
 * on screen, nothing dropped. */
.uir-preset-compact .uir-mini-card {
  padding: var(--uir-role-pad, var(--uir-space-sm));
  gap: 0;
}
.uir-preset-compact .uir-card-title { font-size: .9375rem; }
.uir-preset-compact .uir-kv { font-size: .875rem; }
.uir-preset-compact .uir-card-image { aspect-ratio: 16 / 9; }
.uir-preset-compact .uir-card-actions { padding-top: var(--uir-space-xs); }

/* note — no media, figure omitted. Reviews and messages: prose, not a
 * record. */
.uir-preset-note .uir-card-image { display: none; }
.uir-preset-note .uir-card-title {
  font-size: .9375rem;
  font-weight: 550;
  color: var(--uir-muted);
}
.uir-preset-note .uir-kv { font-size: 1rem; line-height: 1.55; }
.uir-preset-note .uir-kv-label { display: none; }

/* ---------------------------------------------------------------
 * NARROWING CONTROLS (Wave U1 Task R4).
 *
 * A collection's own filters, search box and sort control. What they
 * DO happens on the server (`app_read`'s `resolve_narrowing`, which
 * refuses any field the collection did not declare); these rules only
 * decide what they look like.
 *
 * A lifecycle filter is a chip strip on a phone and a TAB SET on a
 * wide screen — one declaration, two placements, which is the design's
 * §3 rule applied to a control rather than to a region. The adapter
 * chooses which; this stylesheet dresses both.
 * --------------------------------------------------------------- */
.uir-narrowing {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--uir-space-sm);
  margin-bottom: var(--uir-space-md);
}

/* chips — the phone form, and the form for any non-lifecycle filter.
 * A horizontal strip that scrolls rather than wraps on a phone: a
 * filter row that grows to three lines pushes the list it narrows off
 * the screen. */
.uir-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--uir-space-xs);
}
[data-uir-class="compact"] .uir-chips {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}
[data-uir-class="compact"] .uir-chips::-webkit-scrollbar { display: none; }
.uir-chip {
  appearance: none;
  font: inherit;
  font-weight: 550;
  white-space: nowrap;
  color: var(--uir-fg);
  background: transparent;
  border: 1px solid var(--uir-border);
  border-radius: 999px;
  padding: .35rem .85rem;
  cursor: pointer;
  transition: background var(--uir-fast) var(--uir-ease),
              border-color var(--uir-fast) var(--uir-ease);
}
.uir-chip:hover { background: var(--uir-tint); border-color: var(--uir-muted); }
/* The chosen chip is FILLED, not merely outlined: "which of these am
 * I looking at" must be answerable at a glance and without colour
 * alone — the fill changes the shape of the row, not just its hue. */
.uir-chip[aria-pressed="true"] {
  color: var(--uir-on-primary);
  background: var(--uir-primary);
  border-color: var(--uir-primary);
}

/* status tabs — the dense form of a LIFECYCLE filter. Underlined
 * rather than filled, so it reads as navigation between views of one
 * list rather than as a toggle on it.
 *
 * The design's admin survey shows counts here ("Ordered (28)"). They
 * are NOT drawn: a count needs a bounded count read and the paged read
 * verbs have none. The control carries a `title` saying so rather than
 * showing a number nobody computed. */
.uir-status-tabs {
  display: flex;
  gap: var(--uir-space-xs);
  border-bottom: 1px solid var(--uir-border);
}
.uir-status-tab {
  appearance: none;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  font: inherit;
  font-weight: 550;
  color: var(--uir-muted);
  padding: var(--uir-space-sm) var(--uir-space-md);
  white-space: nowrap;
  cursor: pointer;
}
.uir-status-tab[aria-selected="true"] {
  color: var(--uir-fg);
  border-bottom-color: var(--uir-primary);
}
.uir-status-tab:hover { color: var(--uir-fg); }

/* search box and sort control — plain, and deliberately so: they sit
 * above the work, not in it. */
.uir-narrow-search,
.uir-narrow-text,
.uir-narrow-sort {
  font: inherit;
  color: var(--uir-fg);
  background: var(--uir-card);
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  padding: .4rem .7rem;
}
.uir-narrow-search { flex: 1 1 12rem; min-width: 0; }

/* ---------------------------------------------------------------
 * REGION ROLES (Wave U1 Task R3).
 *
 * Five roles an author marks on a node (`toolbar`, `filter_bar`,
 * `summary`, `media`, `footer`) and NEVER places. Placement is
 * derived: the renderer folds each role to the rung the shell
 * selected — a `toolbar` reads with the title on a phone and on a
 * console, a `summary` is the side pane on a console — so by the time
 * a container reaches this stylesheet the only question left is what
 * it LOOKS like.
 *
 * A role with no members is omitted from the IR and its container is
 * removed from the DOM, so nothing below can leave an empty band.
 * --------------------------------------------------------------- */

/* toolbar — screen-level controls in one row: a primary "new", a
 * search box, a view switcher. Wraps rather than scrolls, for the
 * same reason a `row` section does. */
.uir-region-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--uir-space-sm);
  margin-bottom: var(--uir-space-md);
}
.uir-region-toolbar > * { margin: 0; }

/* media — a full-bleed band at the top of the page. Bleeds past the
 * page gutter rather than sitting inside it: a lead image with a
 * margin reads as an illustration, not as the page's opening. */
.uir-region-media {
  margin-inline: calc(-1 * var(--uir-space-lg));
  margin-bottom: var(--uir-space-lg);
}
.uir-region-media .uir-card-image,
.uir-region-media .uir-image { border-radius: 0; }

/* filter_bar — what narrows the collection below it, immediately
 * above that collection on every shell. Horizontal, and on a phone
 * STICKY under the header: scrolling a long list must not scroll away
 * the control that shortens it. */
.uir-region-filter_bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--uir-space-sm);
  margin-bottom: var(--uir-space-md);
}
[data-uir-class="compact"] .uir-region-filter_bar {
  position: sticky;
  top: 0;
  z-index: 4;
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  background: var(--uir-bg);
  padding-block: var(--uir-space-sm);
  border-bottom: 1px solid var(--uir-border);
}
[data-uir-class="compact"] .uir-region-filter_bar::-webkit-scrollbar { display: none; }

/* summary — a running total the tenant acts on. On a phone it is a
 * bar fixed above the bottom navigation, which is where a cart total
 * belongs and where a thumb already is; the page keeps room for it so
 * the last row of content stays reachable rather than sitting under
 * it, the same inset the bottom bar itself takes.
 *
 * ONLY under the bottom-navigation form. Fixing it on a shell that
 * has no bottom bar would park it over content for no reason, and the
 * renderer has already folded `summary` into the side pane on a
 * console and into the main flow on a page — so on those shells this
 * container is simply a block, styled by the rules its contents
 * already carry. */
.uir-region-summary { margin-top: var(--uir-space-md); }
#app.uir-navigation-bottom_navigation .uir-region-summary:not(:empty) {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(4rem + env(safe-area-inset-bottom, 0px));
  z-index: 5;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--uir-space-sm);
  padding: var(--uir-space-sm) var(--uir-space-md);
  background: var(--uir-card);
  border-top: 1px solid var(--uir-border);
  box-shadow: var(--uir-shadow-md);
}
/* The extra room the bar needs, on top of the room the bottom
 * navigation already takes. `:has` so a screen with no summary keeps
 * exactly the inset it had before this task. */
#app.uir-navigation-bottom_navigation:has(.uir-region-summary:not(:empty)) {
  padding-bottom: calc(8rem + env(safe-area-inset-bottom, 0px));
}

/* footer — the page foot: link columns, secondary navigation, the
 * small print. Last on every shell, and quiet: a footer that competes
 * with the work above it is a footer in the wrong place. */
.uir-region-footer {
  display: flex;
  flex-wrap: wrap;
  gap: var(--uir-space-lg);
  margin-top: var(--uir-space-xl);
  padding-top: var(--uir-space-lg);
  border-top: 1px solid var(--uir-border);
  color: var(--uir-muted);
  font-size: .875rem;
}
.uir-region-footer > * { flex: 1 1 min(14rem, 100%); }

/* ---------------------------------------------------------------
 * LAYOUT FORMS (Wave U1 Task R2).
 *
 * Two axes the universal-layout design's §3 adds to the authored
 * grammar: a SECTION FORM (`stack | row | tabs` on a heading) and a
 * COLLECTION FORM (`list | grid | carousel | feed` on a list or a
 * card collection). The renderer passes both through unchanged; the
 * adapter puts `uir-section-<form>` on the heading and
 * `uir-arrangement-<form>` on the collection, BESIDE the role class,
 * so every rule above still applies and a form can only add to it.
 *
 * `stack` and the two per-kind defaults (`list` for a list, `grid` for
 * a card collection) are what the stylesheet already does, so they get
 * no rules — a rule restating the default is a second place for the
 * default to drift.
 * --------------------------------------------------------------- */

/* row — the heading and the run it owns, side by side. WRAPS rather
 * than scrolls: a row that overflows must not put content out of
 * reach, and a sideways scroll is the carousel's answer, which is a
 * collection's to give. `flex-wrap` also means a phone gets a stack
 * with no separate rule and no breakpoint. */
.uir-section-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--uir-space-md);
}
.uir-section-row > .uir-section { flex-basis: 100%; }
.uir-section-row > :not(.uir-section) { flex: 1 1 min(18rem, 100%); }

/* tabs — a strip of headings over one visible panel. The inactive
 * panels stay in the DOM carrying `hidden`, which assistive
 * technology honours the same way it honours a closed disclosure: the
 * content is reachable by switching tabs, never deleted. */
.uir-tablist {
  display: flex;
  gap: var(--uir-space-xs);
  overflow-x: auto;
  scrollbar-width: none;
  border-bottom: 1px solid var(--uir-border);
  margin-bottom: var(--uir-space-md);
}
.uir-tablist::-webkit-scrollbar { display: none; }
.uir-tab {
  appearance: none;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  font: inherit;
  font-weight: 550;
  color: var(--uir-muted);
  padding: var(--uir-space-sm) var(--uir-space-md);
  white-space: nowrap;
  cursor: pointer;
}
.uir-tab[aria-selected="true"] {
  color: var(--uir-fg);
  border-bottom-color: var(--uir-primary);
}
.uir-tab:hover { color: var(--uir-fg); }
/* The panel's own heading is redundant beside the tab that names it,
 * and reading the same words twice is how a tab set stops reading as
 * one. The heading STAYS in the DOM — its description, when the
 * author wrote one, is content — so only the duplicated `h2` is
 * folded away. */
.uir-tabpanel > .uir-section > h2 { display: none; }
.uir-tabpanel > .uir-section:empty { display: none; }

/* feed — one member per line, read rather than scanned. Applies to
 * both kinds: a card collection's auto-fill grid becomes a single
 * column, and a compact list's card container already is one.
 *
 * NOT BUILT, and recorded rather than faked: the design names a
 * "load more" control at the foot of a feed. That needs a CURSOR, and
 * the renderer's `ReadPage` effect carries `source`/`where`/`limit`
 * and no cursor at all. A control that re-read the same first page
 * would look like paging and not be it, so `feed` ships as the
 * single-column arrangement and the load-more stays inexpressible. */
.uir-arrangement-feed {
  grid-template-columns: 1fr;
  gap: var(--uir-space-md);
}

/* carousel — a horizontal strip the reader scrolls SIDEWAYS instead
 * of a wrapping grid: an offers rail, a "more like this" row (study
 * §9c ruling 2, landed here on the COLLECTION-form axis per the
 * universal-layout design §3 rather than as a card preset).
 *
 * COMPACT ONLY, and that is the ruling, not a shortcut: on a phone a
 * rail is how you fit eight offers in the height of two, and the
 * sideways gesture is native. On a wide screen the same eight offers
 * FIT, and hiding six of them behind a scroll the reader cannot see
 * the end of is worse than showing them — so at medium and expanded
 * the grid rule above simply keeps applying and nothing here
 * overrides it. That is why every rule below is nested inside the
 * compact class rather than being written generally and undone later.
 *
 * `scroll-snap` rather than free scroll: a rail that stops between two
 * cards reads as broken. `mandatory` on the strip, `start` on each
 * card, so a flick always lands on a card edge.
 *
 * No visible scrollbar chrome, for the same reason the bottom
 * navigation strip drops it — the partly-visible next card IS the
 * affordance, which is why the cards do not stretch to fill the row. */
[data-uir-class="compact"] .uir-arrangement-carousel {
  display: grid;
  grid-auto-flow: column;
  /* A fixed basis, never a share of the viewport: a share shrinks
   * without limit as the rail grows, and the whole point of the rail
   * is that adding a ninth offer costs no vertical space and no card
   * width. 72% leaves the next card's edge visible at every phone
   * width, which is the affordance. */
  grid-auto-columns: min(72%, 17rem);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  /* The rail bleeds to both screen edges while its cards keep the
   * page's own gutter — a rail that stops short of the edge reads as
   * a clipped grid rather than as something that continues. */
  scroll-padding-inline: var(--uir-space-md);
  padding-inline: var(--uir-space-md);
  margin-inline: calc(-1 * var(--uir-space-md));
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
[data-uir-class="compact"] .uir-arrangement-carousel::-webkit-scrollbar { display: none; }
[data-uir-class="compact"] .uir-arrangement-carousel .uir-mini-card {
  scroll-snap-align: start;
  /* Equal heights across the rail: cards whose descriptions differ in
   * length must still line their action buttons up, or the rail reads
   * as ragged. */
  height: 100%;
}
/* Reduced motion: no snapping and no smooth glide. Snap is a MOTION
 * the reader did not ask for — a flick that keeps travelling after
 * the finger stops is exactly the class of movement this preference
 * exists to switch off. The rail still scrolls and every card is
 * still reachable; only the assisted motion goes. */
@media (prefers-reduced-motion: reduce) {
  [data-uir-class="compact"] .uir-arrangement-carousel {
    scroll-snap-type: none;
    scroll-behavior: auto;
  }
  [data-uir-class="compact"] .uir-arrangement-carousel .uir-mini-card {
    scroll-snap-align: none;
  }
}

/* ---------------------------------------------------------------
 * Storefront card PRESENTATION on a phone (Task U1-R1, study §9c).
 *
 * The reference the owner named (m.dominos.co.in at 390px) puts one
 * product per row: a full-width photo, the name under it, a muted
 * description, then the price large on a row that ends with the
 * primary "Add +" button. Nothing below is a new preset — `menu` and
 * `media_side` already MEAN "a catalogue card"; these rules say what
 * that card looks like when there is only a phone's width to say it
 * in.
 *
 * COMPACT ONLY, deliberately. `media_side` means "media LEFT", and it
 * still does at medium and expanded, where 5.5rem of thumbnail beside
 * the text is the right density. On a phone the same thumbnail is a
 * stamp beside a paragraph, so the picture takes the width instead —
 * the same content, re-arranged, which is exactly what a responsive
 * class is for and why no rule here is written generally and undone
 * later.
 *
 * NOTHING IS HIDDEN, and that is a deviation from the reference worth
 * naming: the reference TRUNCATES its description to two lines with an
 * ellipsis, and these rules do not. This stylesheet's standing rule
 * (see the preset block above) is that no rule hides a field a tenant
 * declared unless the preset's own grammar row says to omit that slot.
 * A clamp hides authored content on a width nobody chose, so the
 * description renders in full, muted and small.
 * --------------------------------------------------------------- */

/* The picture leads and takes the whole card width. `media_side`'s
 * two-column grid is undone rather than overridden piecemeal: half a
 * grid is worse than either shape. */
[data-uir-class="compact"] .uir-preset-media_side .uir-mini-card {
  display: flex;
  flex-direction: column;
  column-gap: 0;
}
[data-uir-class="compact"] .uir-preset-media_side .uir-card-image {
  grid-row: auto;
  margin: calc(-1 * var(--uir-space-md)) calc(-1 * var(--uir-space-md))
          var(--uir-space-sm);
  width: calc(100% + 2 * var(--uir-space-md));
  /* A dish is photographed wide, not square — 1:1 crops the plate. */
  aspect-ratio: 16 / 9;
  border-radius: var(--uir-radius-md) var(--uir-radius-md) 0 0;
}
[data-uir-class="compact"] .uir-preset-media_side .uir-mini-card > :not(.uir-card-image) {
  grid-column: auto;
}
[data-uir-class="compact"] .uir-preset-menu .uir-card-image {
  aspect-ratio: 16 / 9;
}

/* The name is what a customer reads first, so it grows with the
 * picture rather than staying at list-cell size. */
[data-uir-class="compact"] .uir-preset-menu .uir-card-title,
[data-uir-class="compact"] .uir-preset-media_side .uir-card-title {
  font-size: 1.125rem;
  font-weight: 650;
}

/* The supporting cells recede — muted, small, and with their labels
 * folded away: "Description: 8 cheesy pull-aparts" reads as debug
 * output on a product card. The label is not REMOVED from the DOM
 * anywhere; it is the money cell below that keeps its own. */
[data-uir-class="compact"] .uir-preset-menu .uir-kv,
[data-uir-class="compact"] .uir-preset-media_side .uir-kv {
  font-size: .875rem;
  color: var(--uir-muted);
  line-height: 1.45;
}
[data-uir-class="compact"] .uir-preset-menu .uir-kv .uir-kv-label,
[data-uir-class="compact"] .uir-preset-media_side .uir-kv .uir-kv-label {
  display: none;
}

/* The price is the second thing read, and now the stylesheet can
 * actually FIND it: `uir-format-money_currency` is the authored
 * format the renderer already resolved, put on the cell by the
 * adapter's `formatClass` (Task U1-R1). Before that hook the honest
 * best was to enlarge every value — see the preset block's own note.
 *
 * `order` puts money last among the cells whatever the author's
 * column order, so the price always sits on the row the action
 * button ends. Ordering is not hiding: every cell still renders. */
[data-uir-class="compact"] .uir-preset-menu .uir-format-money_currency,
[data-uir-class="compact"] .uir-preset-media_side .uir-format-money_currency {
  order: 1;
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--uir-fg);
  font-variant-numeric: tabular-nums;
  align-self: center;
}

/* The action closes the card, aligned to the end and wearing the
 * primary look. This is CSS, not a role change: the renderer still
 * emits `action.row`, which is what it IS — a per-record action —
 * and a DSP that restyles `action.row` still reaches it. */
[data-uir-class="compact"] .uir-preset-menu .uir-card-actions,
[data-uir-class="compact"] .uir-preset-media_side .uir-card-actions {
  order: 2;
  justify-content: flex-end;
  align-items: center;
  margin-top: calc(-1 * var(--uir-space-lg));
  padding-top: 0;
}
[data-uir-class="compact"] .uir-preset-menu .uir-card-actions .uir-action-row,
[data-uir-class="compact"] .uir-preset-media_side .uir-card-actions .uir-action-row {
  color: var(--uir-on-primary);
  background: var(--uir-primary);
  border-color: var(--uir-primary);
  font-weight: 600;
  padding: .5rem 1.1rem;
  margin-right: 0;
}

/* tile — the category grid. `tile` omits media by its grammar row
 * because the KPI strip it was written for has no picture to omit; a
 * collection that DECLARES an `image_field` has one, and hiding it
 * would drop the only thing an image tile exists to show. So the rule
 * is conditional on the picture existing rather than unconditional,
 * and a KPI tile (no `image_field`, therefore no `img` in the DOM at
 * all) renders exactly as it did before.
 *
 * Three per row on a phone is the reference's own count and the one
 * that keeps a category label readable at 390px. */
[data-uir-class="compact"] .uir-card_collection.uir-preset-tile,
[data-uir-class="compact"] .uir-list-cards.uir-preset-tile {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--uir-space-sm);
}
.uir-preset-tile .uir-mini-card:has(.uir-card-image) .uir-card-image {
  display: block;
  margin: calc(-1 * var(--uir-space-sm)) calc(-1 * var(--uir-space-sm))
          var(--uir-space-xs);
  width: calc(100% + 2 * var(--uir-space-sm));
  /* Square: a category thumbnail is an icon, not a scene. */
  aspect-ratio: 1 / 1;
  border-radius: var(--uir-radius-md) var(--uir-radius-md) 0 0;
}
.uir-preset-tile .uir-mini-card:has(.uir-card-image) {
  align-items: center;
  text-align: center;
}
.uir-preset-tile .uir-mini-card:has(.uir-card-image) .uir-card-title {
  order: 0;
  font-size: .8125rem;
  font-weight: 600;
  color: var(--uir-fg);
}

/* Pictures: the one element that separates a storefront from a
 * spreadsheet.
 *
 * `.uir-image` is the FIGURE the adapter wraps around either an <img>
 * or the absent-placeholder <p> — not the image itself. Putting the
 * 4:3 box on the figure sized the PLACEHOLDER to a full product photo,
 * which is how "no picture yet" became the largest thing on an
 * operations screen. The aspect ratio belongs on the img. */
.uir-image {
  margin: 0;
  min-width: 0;
}
.uir-image > img,
.uir-image-img,
.uir-card-image {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--uir-tint);
  border-radius: var(--uir-radius-md);
}
.uir-mini-card .uir-card-image {
  margin: calc(-1 * var(--uir-space-md)) calc(-1 * var(--uir-space-md))
          var(--uir-space-sm);
  width: calc(100% + 2 * var(--uir-space-md));
  border-radius: var(--uir-radius-md) var(--uir-radius-md) 0 0;
}
/* Merge reconciliation: the renderer's Image element carries an optional
   caption; the figure's own rules above size the picture. */
.uir-image-caption {
  margin-top: var(--uir-space-sm);
  color: var(--uir-muted);
  font-size: .85rem;
}
.uir-image-absent,
.uir-image-fallback {
  display: flex;
  align-items: center;
  gap: var(--uir-space-xs);
  min-height: 2.5rem;
  padding: var(--uir-space-sm) var(--uir-space-md);
  background: var(--uir-tint);
  border-radius: var(--uir-radius-sm);
  color: var(--uir-muted);
  font-size: .875rem;
}

/* ---------------------------------------------------------------
 * One record: definition grid, labels quiet, values loud.
 * --------------------------------------------------------------- */
.uir-detail {
  display: grid;
  grid-template-columns: minmax(6rem, max-content) 1fr;
  gap: var(--uir-space-xs) var(--uir-space-md);
  margin: 0;
}
.uir-detail dt {
  color: var(--uir-muted);
  font-size: .875rem;
  padding-top: .1em;
}
.uir-detail dd { margin: 0; font-weight: 500; }
/* A record with no value yet must read as "nothing here", not as a
 * label with its value missing — an empty <dd> renders as blank space
 * and the whole panel looks like a broken list of words. */
.uir-detail dd:empty::after {
  content: "—";
  color: var(--uir-muted);
  font-weight: 400;
}

/* A number read at a glance. */
.uir-stat { padding: var(--uir-space-sm) 0; }
/* AD-R2 defect 7: a stat with NO value is a ROW — label left,
 * placeholder right — not a heading with nothing under it. The live
 * cart page stacked six valueless stats and read as a table of
 * contents rather than as a bill waiting to be filled in.
 *
 * The row still SHOWS: a tenant waiting for a total needs to see where
 * it will land, and "no number yet" is a different fact from "this
 * line does not exist". */
.uir-stat-empty {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--uir-space-md);
  padding-block: var(--uir-space-xs);
  border-bottom: 1px solid var(--uir-border);
}
.uir-stat-empty .uir-stat-label { margin-bottom: 0; }
.uir-stat-empty .uir-stat-value {
  font-size: 1rem;
  font-weight: 500;
  color: var(--uir-muted);
}
.uir-stat-label {
  display: block;
  color: var(--uir-muted);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .02em;
  text-transform: uppercase;
  margin-bottom: .15em;
}
.uir-stat-value {
  display: block;
  font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.125rem);
  font-weight: 650;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

/* AD-R5: a cell that carries a semantic STATUS role reads as a small
 * badge rather than as prose. Two things arrive here: a declared
 * status `presentation` (which the IR has carried since Phase 02 and
 * which reached no card until now) and a Boolean's derived tone —
 * "Available" / "Not Available" where the live menu printed `true`.
 *
 * The same tone tokens the `badge` element uses, so one app does not
 * have two vocabularies of colour for one idea. `ready` is the
 * positive state; `neutral` is its absence and is deliberately quiet —
 * an item off today's menu is a fact, not an alarm. */
.uir-kv .uir-status-ready,
.uir-kv .uir-status-neutral,
.uir-kv .uir-status-progress,
.uir-kv .uir-status-warning,
.uir-kv .uir-status-critical {
  display: inline-block;
  padding: .1rem .5rem;
  border-radius: 999px;
  font-size: .8125rem;
  font-weight: 600;
}
.uir-kv .uir-status-ready { background: var(--uir-ok-bg); color: var(--uir-ok-fg); }
.uir-kv .uir-status-progress,
.uir-kv .uir-status-warning,
.uir-kv .uir-status-critical {
  background: var(--uir-warn-bg);
  color: var(--uir-warn-fg);
}
.uir-kv .uir-status-neutral { background: var(--uir-tint); color: var(--uir-muted); }

/* State, as colour that means something. */
.uir-badge {
  display: inline-flex;
  align-items: center;
  gap: .35em;
  padding: .2em .7em;
  border-radius: var(--uir-radius-full);
  border: 1px solid transparent;
  font-size: .8125rem;
  font-weight: 550;
  line-height: 1.6;
  white-space: nowrap;
}
.uir-badge-label { color: var(--uir-muted); font-weight: 500; }
/* A chip with a label and no value read as "Request Status:" floating on
 * its own — a label with nothing to label. Same rule as an empty <dd>:
 * say "nothing here" rather than trail off. */
.uir-badge > span:last-child:empty::after { content: "—"; opacity: .7; }
.uir-badge-tone-success { background: var(--uir-ok-bg); color: var(--uir-ok-fg); }
.uir-badge-tone-warning,
.uir-badge-tone-danger { background: var(--uir-warn-bg); color: var(--uir-warn-fg); }
.uir-badge-tone-info {
  background: var(--uir-accent-tint);
  color: var(--uir-primary);
  border-color: color-mix(in srgb, var(--uir-primary) 25%, transparent);
}
.uir-badge-tone-neutral {
  background: var(--uir-tint);
  color: var(--uir-muted);
  border-color: var(--uir-border);
}

/* ---------------------------------------------------------------
 * Forms.
 * --------------------------------------------------------------- */
/* Forms get a MEASURE. Without one, a single-column form on a wide
 * screen stretches every input to the full card — a 1000px-wide box for
 * a person's name, which reads as an unfinished layout however well the
 * rest is spaced. Short fields stay short; only prose grows. */
.uir-form { display: flex; flex-direction: column; gap: var(--uir-space-sm); }
.uir-card > .uir-form,
.uir-card > form { max-width: 34rem; }
.uir-card > .uir-form textarea,
.uir-card > form textarea { max-width: 100%; }
.uir-card label {
  display: block;
  font-weight: 550;
  font-size: .9375rem;
  margin-bottom: .3em;
}
.uir-card input,
.uir-card textarea,
.uir-card select {
  width: 100%;
  box-sizing: border-box;
  padding: .6rem .75rem;
  font: inherit;
  color: var(--uir-fg);
  background: var(--uir-bg);
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  transition: border-color var(--uir-fast) var(--uir-ease),
              box-shadow var(--uir-fast) var(--uir-ease);
}
.uir-card input:hover,
.uir-card textarea:hover,
.uir-card select:hover { border-color: var(--uir-muted); }
.uir-card input:focus,
.uir-card textarea:focus,
.uir-card select:focus {
  outline: none;
  border-color: var(--uir-primary);
  box-shadow: 0 0 0 3px var(--uir-accent-tint);
}
.uir-card textarea { min-height: 6.5rem; resize: vertical; }
.uir-card input::placeholder,
.uir-card textarea::placeholder { color: var(--uir-muted); opacity: .8; }

/* Phase 04 Wave A Task A2: a Reference field's picker shape — a text
 * entry plus a browse affordance, so it reads as "this points at
 * admitted content" rather than a plain free-text box, without
 * pretending a live catalog lookup exists yet (that's Phase 12's
 * typeahead). The browse button stays visibly disabled — a `title`/
 * `aria-label` carries why (adapter-side, locale-keyed) — so nothing
 * here LOOKS clickable-and-broken. Shares the `.uir-card input`
 * styling above (still a real `<input>`), no separate width/padding
 * rules needed for the field itself.
 *
 * Fix round 1, M3: the muted color/cursor is scoped to `:disabled`
 * (not the bare class) — this button is ALWAYS rendered disabled
 * today (Phase 12 owns turning it live), but a future phase that
 * enables it must not have to remember to also delete a stale
 * always-on "disabled-looking" rule here. The base class carries only
 * layout/shape, which applies in either state. */
.uir-reference-picker {
  display: flex;
  align-items: stretch;
  gap: var(--uir-space-xs);
}
.uir-reference-picker .uir-reference-input { flex: 1 1 auto; }
.uir-reference-browse {
  flex: 0 0 auto;
  appearance: none;
  font: inherit;
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  padding: 0 .75rem;
}
.uir-reference-browse:disabled {
  color: var(--uir-muted);
  background: var(--uir-tint);
  cursor: not-allowed;
}

.uir-field-group {
  border: 0;
  padding: 0;
  margin: 0 0 var(--uir-space-md);
  display: flex;
  flex-direction: column;
  gap: var(--uir-space-sm);
}
.uir-field-group legend {
  padding: 0;
  margin-bottom: var(--uir-space-xs);
  font-weight: 600;
  color: var(--uir-fg);
}

/* ---------------------------------------------------------------
 * The stepper (UIR-1) — one commitment flow, several steps.
 *
 * Every rule below uses the tokens the rest of this sheet already
 * reads, so a tenant Design System Profile restyles a stepper by
 * restyling nothing: it inherits the same surface, border, spacing
 * and primary colour a form does.
 * --------------------------------------------------------------- */
.uir-stepper-progress {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--uir-space-sm);
  list-style: none;
  margin: 0 0 var(--uir-space-lg);
  padding: 0;
}
.uir-stepper-step {
  display: flex;
  align-items: center;
  gap: var(--uir-space-xs);
  color: var(--uir-muted);
  font-size: .875rem;
}
/* The connector between one step and the next. A pseudo-element, so
 * the markup carries only the steps themselves — there is no such
 * thing as "the gap between steps" for a screen reader to read out. */
.uir-stepper-step:not(:last-child)::after {
  content: "";
  flex: 1 0 1.5rem;
  height: 1px;
  margin-left: var(--uir-space-xs);
  background: var(--uir-border);
}
.uir-stepper-ordinal {
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  flex: 0 0 auto;
  border: 1px solid var(--uir-border);
  border-radius: 999px;
  font-size: .75rem;
  font-weight: 600;
  line-height: 1;
}
/* Three states, and they must differ by more than colour: the active
 * step's title also gains weight and full-contrast text, so the
 * position is readable without colour vision. */
.uir-stepper-step.is-done .uir-stepper-ordinal {
  border-color: var(--uir-primary);
  color: var(--uir-primary);
}
.uir-stepper-step.is-active .uir-stepper-ordinal {
  border-color: var(--uir-primary);
  background: var(--uir-primary);
  color: var(--uir-on-primary);
}
.uir-stepper-step.is-active {
  color: var(--uir-fg);
  font-weight: 600;
}
.uir-stepper-fields {
  border: 0;
  padding: 0;
  margin: 0 0 var(--uir-space-md);
  display: flex;
  flex-direction: column;
  gap: var(--uir-space-sm);
}
/* A folded step is hidden, never removed — see the adapter's own note:
 * its controls stay in the document so the final submit carries them.
 * `display: none` rather than the UA default so the flex rules above
 * cannot resurrect it. */
.uir-stepper-fields[hidden] { display: none; }
.uir-stepper-fields legend {
  padding: 0;
  margin-bottom: var(--uir-space-xs);
  font-weight: 600;
  color: var(--uir-fg);
}
.uir-stepper-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--uir-space-sm);
  margin-top: var(--uir-space-md);
}
/* Back reads as the quiet option — it takes the same shape as every
 * other secondary button on the page rather than a shape of its own. */
.uir-stepper-back {
  appearance: none;
  font: inherit;
  font-weight: var(--uir-role-weight, 550);
  color: var(--uir-fg);
  background: transparent;
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  padding: .35rem .8rem;
  cursor: pointer;
  transition: background var(--uir-fast) var(--uir-ease),
              border-color var(--uir-fast) var(--uir-ease);
}
.uir-stepper-back:disabled {
  opacity: .5;
  cursor: not-allowed;
}
/* The forward control sits at the end of the row whether it advances
 * or commits, so the place a tenant presses does not move under them
 * on the last step. `align-self`/`margin-top` undo `.uir-action-primary`'s
 * standalone-in-a-column defaults, which this row does not want. */
.uir-stepper-nav .uir-action-primary {
  align-self: auto;
  margin-top: 0;
  margin-left: auto;
}

/* ---------------------------------------------------------------
 * Actions.
 * --------------------------------------------------------------- */
.uir-action-primary {
  align-self: flex-start;
  appearance: none;
  border: 0;
  padding: .6rem 1.2rem;
  font: inherit;
  /* The emphasis half of the same variant hook — see `.uir-mini-card`.
   * A DSP that resolves `action.primary` to `emphasis: low` gets a
   * quieter button without a second rule anywhere. */
  font-weight: var(--uir-role-weight, 600);
  color: var(--uir-on-primary);
  background: var(--uir-primary);
  border-radius: var(--uir-radius-sm);
  box-shadow: var(--uir-shadow-sm);
  cursor: pointer;
  margin-top: var(--uir-space-sm);
  transition: background var(--uir-fast) var(--uir-ease),
              box-shadow var(--uir-fast) var(--uir-ease),
              transform var(--uir-fast) var(--uir-ease);
}
.uir-action-primary:hover {
  background: var(--uir-primary-hover);
  box-shadow: var(--uir-shadow-md);
}
.uir-action-primary:active { transform: translateY(1px); box-shadow: none; }

.uir-action-row,
.uir-action-secondary,
.uir-action-row_standalone,
.uir-action-standalone {
  appearance: none;
  font: inherit;
  font-weight: var(--uir-role-weight, 550);
  color: var(--uir-fg);
  background: transparent;
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  padding: .35rem .8rem;
  margin-right: var(--uir-space-xs);
  cursor: pointer;
  transition: background var(--uir-fast) var(--uir-ease),
              border-color var(--uir-fast) var(--uir-ease);
}
.uir-action-row:hover,
.uir-action-secondary:hover,
.uir-action-row_standalone:hover,
.uir-action-standalone:hover {
  background: var(--uir-tint);
  border-color: var(--uir-muted);
}
.uir-action-row_standalone { margin: var(--uir-space-sm) 0; }

/* ---------------------------------------------------------------
 * Feedback. An empty screen is an invitation, not a shrug — so the
 * empty state gets a real surface rather than grey text in the void.
 * --------------------------------------------------------------- */
.uir-feedback-empty,
.uir-empty {
  display: grid;
  /* AD-R2 defect 5: an empty collection says so ONCE, across the whole
   * collection — never as one cell of the grid it is standing in for.
   * The storefront's empty offers rail rendered its placeholder as a
   * tile inside the category grid, where it read as a nameless
   * category. Harmless outside a grid: `grid-column` on a non-grid
   * child is ignored. */
  grid-column: 1 / -1;
  place-items: center;
  gap: var(--uir-space-xs);
  padding: var(--uir-space-lg) var(--uir-space-md);
  margin: 0;
  color: var(--uir-muted);
  text-align: center;
  font-size: .9375rem;
  /* No dashed border: a dashed rectangle reads as a drop zone or a
   * broken image, and an empty table is neither — it is a table that
   * has no rows yet. */
  background: var(--uir-tint);
  border-radius: var(--uir-radius-sm);
}
.uir-feedback-loading {
  color: var(--uir-muted);
  padding: var(--uir-space-lg) 0;
  text-align: center;
}
.uir-feedback-error { color: var(--uir-danger); font-weight: 550; }

.uir-feedback-receipt,
.uir-feedback-refusal,
.uir-refusal {
  border-radius: var(--uir-radius-md);
  padding: var(--uir-space-md);
  margin-bottom: var(--uir-space-md);
  border-left: 3px solid currentcolor;
}
.uir-feedback-receipt { background: var(--uir-ok-bg); color: var(--uir-ok-fg); }
.uir-feedback-refusal,
.uir-refusal { background: var(--uir-warn-bg); color: var(--uir-warn-fg); }

/* Phase 04 Wave C Task C3 (`bug_fixed.md` B-225 — filed as B-190 and
 * renumbered 2026-08-28, see `bug.md` B-221): the feedback element.
 * Until this branch existed in the adapter, EVERY `Element::Feedback`
 * — stale, offline, retry, pending, confirmation, conflict, and the
 * failure arm of every dispatch — rendered as a developer placeholder
 * string, so none of these classes had anything to style. Roles are
 * grouped by what they tell the tenant: work that did not land reads
 * at full weight; a note about the data's freshness reads quietly. */
.uir-feedback-conflict,
.uir-feedback-failure {
  border-radius: var(--uir-radius-md);
  padding: var(--uir-space-md);
  margin-bottom: var(--uir-space-md);
  border-left: 3px solid currentcolor;
  background: var(--uir-warn-bg);
  color: var(--uir-warn-fg);
}
/* The conflict is the one feedback role that reports someone else's
 * write beating the tenant's: it gets the danger accent so it is not
 * read as the same thing as a guard declining. */
.uir-feedback-conflict { border-left-color: var(--uir-danger); }
.uir-feedback-confirmation,
.uir-feedback-offline,
.uir-feedback-pending,
.uir-feedback-retry,
.uir-feedback-stale {
  color: var(--uir-muted);
  margin-bottom: var(--uir-space-sm);
  font-size: .9375rem;
}
.uir-feedback-message { margin: 0; }
.uir-feedback-refresh { margin-top: var(--uir-space-sm); }

/* Phase 04 Wave A Task A3 (honest screens): the toast element — same
 * tone palette as the badge tones above, reused rather than inventing
 * a second one. Inline (not fixed-position): this adapter renders
 * toasts into the SAME region as every other element (see
 * uir-adapter.js's "toast" branch doc for why); a future task that
 * wants a floating overlay position can restyle this class without
 * touching the Rust IR or the branch that emits it. */
.uir-feedback-toast {
  border-radius: var(--uir-radius-md);
  padding: var(--uir-space-sm) var(--uir-space-md);
  margin-bottom: var(--uir-space-md);
  font-size: .9375rem;
}
.uir-toast-tone-success { background: var(--uir-ok-bg); color: var(--uir-ok-fg); }
.uir-toast-tone-warning,
.uir-toast-tone-danger { background: var(--uir-warn-bg); color: var(--uir-warn-fg); }

.uir-transient-notice { position: relative; padding-right: 2.6rem; }
.uir-dismiss {
  position: absolute;
  top: .4rem;
  right: .5rem;
  background: none;
  border: 0;
  color: inherit;
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
  padding: .15rem .45rem;
  border-radius: var(--uir-radius-sm);
}
.uir-dismiss:hover { background: rgb(255 255 255 / 35%); }

.uir-unrendered {
  color: var(--uir-muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .8125rem;
  padding: var(--uir-space-sm);
  border: 1px dashed var(--uir-border);
  border-radius: var(--uir-radius-sm);
}

/* ---------------------------------------------------------------
 * Layout patterns chosen by the renderer (render::auto_pattern).
 * The renderer decides WHICH arrangement; this decides how it looks.
 * --------------------------------------------------------------- */
.uir-layout { display: flex; flex-direction: column; gap: var(--uir-space-md); }
.uir-region-body { display: grid; gap: var(--uir-space-lg); align-items: start; }
.uir-region-main, .uir-region-side { min-width: 0; }

.uir-pattern-master_detail .uir-region-body { grid-template-columns: 1fr; }
.uir-pattern-split .uir-region-body { grid-template-columns: 1fr; }
.uir-pattern-grid .uir-region-main {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: var(--uir-space-md);
}

/* Two-column arrangements switch on the class the ADAPTER publishes
 * (`data-uir-class`, from `computeResponsiveClass`) — never on a media
 * query of this file's own. The renderer already decides what counts as
 * a wide screen; a second breakpoint table here would drift from it, and
 * did (60rem vs the adapter's 1024px left a 64px band where the two
 * disagreed). Column COUNTS still need no breakpoints at all: the grids
 * above size intrinsically with auto-fill + minmax, so the browser fits
 * as many as the width allows. */
[data-uir-class="expanded"] .uir-pattern-master_detail .uir-region-body {
  /* The record being read sits beside the list it came from — the
   * pairing master_detail exists for. Side is narrower and sticks, so
   * scrolling a long list keeps the record in view. */
  grid-template-columns: minmax(0, 1.9fr) minmax(18rem, 1fr);
}
[data-uir-class="expanded"] .uir-pattern-split .uir-region-body {
  grid-template-columns: minmax(0, 1fr) minmax(18rem, 1fr);
}
[data-uir-class="expanded"] .uir-pattern-master_detail .uir-region-side,
[data-uir-class="expanded"] .uir-pattern-split .uir-region-side {
  position: sticky;
  top: var(--uir-space-md);
}

/* ---------------------------------------------------------------
 * Wave LAYOUT-1 — the twelve-column grid.
 *
 * The renderer decides the rows and the spans (a screen's job recipe,
 * or its authored `layout:`); this only decides how twelve columns
 * look. One grid per ROW rather than one grid for the whole body,
 * because each row is its own twelve — a row of 8+4 above a row of
 * 3+3+3+3 is two independent divisions, not one grid with a span
 * pattern to work out.
 *
 * `grid-column: span N` is written by the adapter as an inline style,
 * because N is data. Nothing here names a span.
 *
 * NO breakpoint of its own, and that is deliberate. The renderer
 * emits `row_pattern: "grid"` only at `medium` and `expanded`, and
 * `"stack"` at `compact` — so the decision about when a grid applies
 * has already been made, once, in Rust. A media query here would be a
 * second breakpoint table, which is exactly the drift the pattern
 * rules above record (60rem versus the adapter's 1024px left a 64px
 * band where the two disagreed).
 * --------------------------------------------------------------- */
.uir-grid { display: flex; flex-direction: column; gap: var(--uir-space-lg); min-width: 0; }
.uir-grid-row {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--uir-space-lg);
  align-items: start;
}
/* `grid-column: span N` is written by the adapter as an INLINE style,
 * because N is data and this file holds no data. All that is left here
 * is the one thing a twelve-column child always needs: permission to
 * be narrower than its content. */
.uir-grid-cell { min-width: 0; }

/* ---------------------------------------------------------------
 * Wave LAYOUT-1 — the hero band.
 *
 * The page's opening block: a headline, optional words, one button.
 * Full-bleed inside whatever cell or region holds it, and quiet —
 * every colour here is a token, so a Design Package that says nothing
 * about heroes still gets one that matches the rest of the app.
 * --------------------------------------------------------------- */
.uir-hero {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--uir-space-sm);
  padding: var(--uir-space-lg);
  border-radius: var(--uir-radius-lg);
  background: var(--uir-card);
  border: 1px solid var(--uir-border);
}
.uir-hero-headline { margin: 0; font-size: 1.75rem; line-height: 1.2; }
.uir-hero-body { margin: 0; color: var(--uir-muted); max-width: 60ch; }
.uir-hero > button { margin-top: var(--uir-space-sm); }

/* ---------------------------------------------------------------
 * Task AC — the linked page's way back.
 *
 * A linked page is reached by clicking a record and is deliberately
 * absent from the tab strip, so without this control the only way
 * back is the browser's own Back button — invisible furniture in an
 * app-shaped view, and absent altogether from an installed PWA. The
 * ADAPTER decides when to draw it (`backAffordanceFor`: a navigation
 * token naming this screen, and a `stack` pattern); this only decides
 * how it looks.
 *
 * Quiet on purpose: a text-weight control, not a button-shaped one. It
 * is a way out of a page, not an action on it, and giving it button
 * chrome would put it in competition with the page's real actions.
 * --------------------------------------------------------------- */
.uir-back {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  margin: 0 0 var(--uir-space-sm);
  padding: .3rem .1rem;
  border: 0;
  background: none;
  color: var(--uir-muted);
  font: inherit;
  font-size: .9rem;
  cursor: pointer;
  /* A real target on a touch screen — the same floor the tab items
   * meet, met here with padding rather than a visible box. */
  min-height: 2.75rem;
}
.uir-back:hover { color: var(--uir-fg); }
.uir-back:focus-visible {
  /* The same focus signal the form controls above use — one token
   * (`--uir-primary`), not a second focus colour invented here. */
  outline: 2px solid var(--uir-primary);
  outline-offset: 2px;
  border-radius: var(--uir-radius-sm);
}
/* Under the bottom-navigation form the content column is the whole
 * screen and the tab strip is a fixed bar at the foot, so the back
 * control is the ONLY navigation chrome at the top of the page. It
 * gets the full row and a little more air, so a thumb reaching the top
 * of a phone screen finds it without hunting. */
#app.uir-navigation-bottom_navigation .uir-back {
  width: 100%;
  justify-content: flex-start;
  margin-bottom: var(--uir-space-md);
}

/* ---------------------------------------------------------------
 * Phase 11 Wave D Task D2 — per-SHELL styling.
 *
 * The renderer has already decided the STRUCTURE for each shell rung
 * (`render::ShellArrangement`): a console keeps header/main/side, a
 * column folds side into main, a page is one flow. These rules only
 * decide how that structure LOOKS, exactly as the `uir-pattern-*`
 * rules above do — nothing here re-parents a node or hides one, and a
 * shell with no rule below simply gets the default look, which is the
 * styling twin of the renderer mapping an unknown shell to its default
 * layout.
 *
 * The class name is pinned from Rust
 * (`surface_tests::the_adapter_and_the_stylesheet_agree_on_the_shell_class`)
 * so renaming it fails `cargo test` rather than silently un-styling a
 * shell nobody would notice until a browser run.
 * --------------------------------------------------------------- */

/* The shell class sits ON the layout element itself (the adapter
 * appends it to `uir-layout uir-pattern-*`), so a rule that targets
 * the layout is `.uir-layout.uir-shell-x`, never a descendant
 * selector — a descendant selector here matches NOTHING, which is the
 * silent kind of wrong a stylesheet never reports. Every class named
 * below is one `uir-adapter.js` actually emits, asserted from Rust by
 * the pin test named above. */

/* A touch shell is one column for one pair of hands: bigger tap
 * targets, more space between things, and no sticky side pane to
 * fight a thumb (the renderer already gave it no side region). */
.uir-shell-touch .uir-region-body { gap: var(--uir-space-lg); }
.uir-shell-touch .uir-card { padding: var(--uir-space-lg); }
.uir-shell-touch button,
.uir-shell-touch .uir-action-primary,
.uir-shell-touch .uir-action-secondary { min-height: 2.75rem; }

/* A dense shell is an operator console: tighter rows, so more of the
 * work is visible at once. */
.uir-shell-dense .uir-region-body { gap: var(--uir-space-md); }
.uir-shell-dense .uir-card { padding: var(--uir-space-sm); }

/* A public shell is a page a visitor reads, not a console they drive:
 * one centred measure, and generous room around it. */
.uir-layout.uir-shell-public {
  max-width: 56rem;
  margin-inline: auto;
}
.uir-shell-public .uir-region-body { gap: var(--uir-space-lg); }

/* ---------------------------------------------------------------
 * The landing hero (Phase 12 Wave A Task A2).
 *
 * The renderer emits a `hero` layout region ONLY when the screen being
 * rendered IS the app's DERIVED landing (`render::hero_ids`), and puts
 * the landing's leading heading/prose/picture run in it. This is what
 * that region looks like; the decision of whether it exists is not
 * made here.
 *
 * FULL-BLEED, WHERE THAT MEANS ANYTHING. A landing's opening block
 * reads as the page, not as one more card in a column — so on a shell
 * whose content column IS the viewport the hero escapes the layout's
 * measure (including the public shell's 56rem centring above) and runs
 * edge to edge, with its own content re-centred inside.
 *
 * Under a persistent sidebar there is no such thing as "the viewport
 * width" for this element, and the rule below says so rather than
 * pretending otherwise — see its own note (AD-R2 defect 1).
 * --------------------------------------------------------------- */
.uir-region-hero:empty { display: none; }
.uir-region-hero {
  position: relative;
  /* LAYOUT-1b: `box-sizing` is the half of this rule that produced a
   * measured 1664px band inside a 1280px viewport. There is no global
   * `box-sizing: border-box` in this file, so the default `content-box`
   * added the 192px of horizontal padding to the declared width on
   * BOTH sides: 1280 + 192 + 192 = 1664, which is the number the live
   * page reported. The padding belongs INSIDE the band. */
  box-sizing: border-box;
  width: 100%;
  padding: var(--uir-space-xl) max(var(--uir-space-lg), calc(50vw - 28rem));
  background: var(--uir-tint);
  border-bottom: 1px solid var(--uir-border);
  display: flex;
  flex-direction: column;
  gap: var(--uir-space-sm);
}
/* FULL BLEED — RETIRED at LAYOUT-1b, and the reason is that a FRAME
 * now exists.
 *
 * This used to read, scoped away from the two sidebar navigation
 * forms:
 *
 *     width: 100vw;
 *     margin-inline: calc(50% - 50vw);
 *
 * That is the standard break-out, and its whole premise is that the
 * band's containing block is CENTRED IN THE VIEWPORT. It had already
 * been scoped once (AD-R2 defect 1: under a persistent sidebar the
 * content is column 2 of a grid, so the bleed ran left and the hero
 * drew on top of the navigation links).
 *
 * SHELL-1 falsified the premise for every remaining shell. The layout
 * is no longer a child of `#app`; it is
 * `.uir-frame-storefront` -> `.uir-frame-body` -> `.uir-layout`, and
 * the frame body is not viewport-centred. Measured live on :9400 at a
 * 1280px viewport with a `land` screen: the band came out 1664px wide
 * against a 1280px layout, and `document.documentElement.scrollWidth`
 * went to 1664 — a horizontal scrollbar on the app's front door.
 *
 * The replacement is `width: 100%` on the rule above: the band fills
 * ITS OWN COLUMN. Now that a frame draws the page, the frame body IS
 * the page column, so "fills the column" and "full bleed" are the same
 * thing there — and it is arithmetic-free, so no viewport and no
 * nesting can make it overflow.
 *
 * The consequence, recorded rather than discovered later: on a
 * `public` shell, whose layout carries `max-width: 56rem`, the hero
 * now spans that centred measure instead of escaping it to the
 * viewport edges. That is the honest reading of a band that is told to
 * fill its column, and it is the trade this defect forces — a band
 * that escapes its column is exactly the thing that widened the page.
 * `.uir-frame-body`'s `overflow-x: clip` below is the structural
 * backstop, so even a future rule that tries to break out cannot add a
 * scrollbar. */
/* The opening statement, sized for a landing rather than for a section
 * heading inside a console. `clamp` so it is large on a laptop without
 * overflowing a 390px phone — the four-script vernacular floor means a
 * Devanagari or Tamil title sets taller than the Latin one this was
 * eyeballed against, which is why the line-height stays generous. */
.uir-region-hero h2,
.uir-region-hero h3 {
  margin: 0;
  font-size: clamp(1.75rem, 1.1rem + 2.4vw, 3rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
}
.uir-region-hero p {
  margin: 0;
  max-width: 42rem;
  font-size: clamp(1rem, .95rem + .3vw, 1.1875rem);
  color: var(--uir-muted);
}
/* A landing picture is the backdrop of the opening block, not a card's
 * 4:3 thumbnail — so it is wide, and it is BEHIND the words rather
 * than above them, which is the content-over-media treatment the
 * vocabulary catalog's `overlay` row points here for. The scrim is the
 * same one the `overlay` card preset uses: text over an arbitrary
 * tenant photograph has no contrast guarantee without it. */
.uir-region-hero .uir-image,
.uir-region-hero .uir-image-img {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  margin: 0;
  aspect-ratio: auto;
  border-radius: 0;
  object-fit: cover;
}
.uir-region-hero:has(.uir-image-img)::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--uir-scrim);
}
.uir-region-hero:has(.uir-image-img) { isolation: isolate; }
.uir-region-hero:has(.uir-image-img) h2,
.uir-region-hero:has(.uir-image-img) h3,
.uir-region-hero:has(.uir-image-img) p { color: var(--uir-on-scrim); }
/* A phone is not a billboard: the same block, less shouting. */
[data-uir-class="compact"] .uir-region-hero {
  padding: var(--uir-space-lg) var(--uir-space-md);
}

/* ---------------------------------------------------------------
 * Icons (Phase 12 Wave A Task A3).
 *
 * Inline SVG the adapter draws from the closed
 * `ubos_cell_graph_schema::IconName` roster — no icon font, no icon
 * library, nothing fetched. Sized in `em` and stroked with
 * `currentColor`, so an icon is the size and the colour of the text it
 * sits beside and a DSP that re-themes a button re-themes its icon
 * with no rule here.
 *
 * `aria-hidden` is set by the adapter, not here: the label beside the
 * icon is the accessible name, and an icon is never the only thing a
 * control says. That is the four-locale floor's requirement as much as
 * a screen reader's — a glyph a Latin reader guesses right is a glyph
 * a Kannada reader guesses at.
 * --------------------------------------------------------------- */
.uir-icon {
  flex: none;
  width: 1em;
  height: 1em;
  vertical-align: -0.125em;
  margin-inline-end: .45em;
}
/* A button is `inline-block` by default, so an inline SVG plus a text
 * node baseline-aligns badly at small sizes. Making the button a flex
 * line puts the icon and the word on one optical row. */
.uir-action-primary,
.uir-action-row,
.uir-action-secondary,
.uir-action-row_standalone,
.uir-action-standalone,
.uir-tab {
  display: inline-flex;
  align-items: center;
}
/* The destructive action is the one place the icon carries weight
 * beyond decoration, so it takes the danger colour the label already
 * has. */
.uir-action-primary .uir-icon-delete,
.uir-action-row .uir-icon-delete,
.uir-action-secondary .uir-icon-delete { color: var(--uir-danger); }

/* Density is a substrate token; honour it rather than emitting it and
 * ignoring it. */
:root[style*="--uir-density: compact"] .uir-card { padding: var(--uir-space-md); }

/* Phone density — same source of truth as above. */
[data-uir-class="compact"] .uir-card {
  padding: var(--uir-space-md);
  border-radius: var(--uir-radius-md);
}
/* A nowrap strip of five verbose tab labels is far wider than a phone.
 * It has to scroll INSIDE itself — `min-width: 0` is the part that
 * actually stops a flex child from widening its parent, and without it
 * `overflow-x: auto` silently does nothing (measured: 207px of page
 * overflow at 390px wide). */
[data-uir-class="compact"] .uir-tabs {
  flex-wrap: nowrap;
  overflow-x: auto;
  min-width: 0;
  max-width: 100%;
  scrollbar-width: none;
}
[data-uir-class="compact"] .uir-tab { white-space: nowrap; }
[data-uir-class="compact"] .uir-detail { grid-template-columns: 1fr; gap: 0; }
[data-uir-class="compact"] .uir-detail dt { margin-top: var(--uir-space-sm); }

/* Quality floor: motion is an enhancement, never a requirement. */
@media (prefers-reduced-motion: reduce) {
  .uir-tab, .uir-mini-card, .uir-action-primary, .uir-action-row,
  .uir-action-secondary, .uir-card input, .uir-card textarea,
  .uir-card select, .uir-list tbody tr, .uir-bounded_grid tbody tr {
    transition: none;
  }
  .uir-mini-card:hover, .uir-action-primary:active { transform: none; }
}

/* The in-DOM confirmation control — replaces window.confirm. */
.uir-confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgb(16 24 40 / 45%);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: var(--uir-space-md);
}
.uir-confirm-dialog {
  background: var(--uir-card);
  color: var(--uir-fg);
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-lg);
  padding: var(--uir-space-lg);
  max-width: 26rem;
  box-shadow: var(--uir-shadow-lg);
}
/* ---------------------------------------------------------------------
   Wave OVERLAY-1: the dialog layer.

   Two kinds, one set of manners. A modal is centred and sized to its
   content; a drawer is pinned to the trailing edge at expanded width
   and becomes a full sheet at compact — where a side panel on a phone
   is a panel with no side to sit beside.

   `z-index` is one above the confirm dialog's so the two can never
   argue about which is on top, and the backdrop scrolls its own
   content rather than the page behind it (a layer that scrolls the
   inert page under it is a layer the tenant can lose).
   --------------------------------------------------------------------- */
.uir-overlay-backdrop {
  position: fixed;
  inset: 0;
  background: rgb(16 24 40 / 45%);
  backdrop-filter: blur(2px);
  display: flex;
  z-index: 1001;
}
.uir-overlay-dialog {
  background: var(--uir-card);
  color: var(--uir-fg);
  border: 1px solid var(--uir-border);
  box-shadow: var(--uir-shadow-lg);
  display: flex;
  flex-direction: column;
  overflow: auto;
}
.uir-overlay-modal {
  margin: auto;
  border-radius: var(--uir-radius-lg);
  padding: var(--uir-space-lg);
  width: min(34rem, calc(100% - 2 * var(--uir-space-md)));
  max-height: calc(100% - 2 * var(--uir-space-md));
}
.uir-overlay-drawer {
  margin-inline-start: auto;
  border-radius: 0;
  border-block: 0;
  border-inline-end: 0;
  padding: var(--uir-space-lg);
  width: min(30rem, 100%);
  height: 100%;
}
.uir-overlay-title {
  margin: 0 0 var(--uir-space-md);
}
.uir-overlay-body {
  flex: 1 1 auto;
}
/* The layer's own submit lives in the footer and carries the form's
   words; the form's inline button is the one that is actually wired,
   so it is HIDDEN rather than removed — removing it would take
   `bindSubmit` and the policy-disabled state with it. */
.uir-overlay-inline-submit {
  display: none;
}
.uir-overlay-footer {
  display: flex;
  gap: var(--uir-space-sm);
  margin-top: var(--uir-space-md);
  justify-content: flex-end;
}
@media (max-width: 640px) {
  /* Compact: a drawer is a full sheet. */
  .uir-overlay-drawer {
    margin: 0;
    width: 100%;
    max-width: none;
  }
  .uir-overlay-modal {
    width: 100%;
    max-height: 100%;
    margin: auto 0 0;
    border-radius: var(--uir-radius-lg) var(--uir-radius-lg) 0 0;
  }
}

.uir-confirm-actions {
  display: flex;
  gap: var(--uir-space-sm);
  margin-top: var(--uir-space-md);
  justify-content: flex-end;
}

/* Print — interactive chrome never belongs on paper; content stays. */
@media print {
  .uir-tabs, .uir-action-primary, .uir-action-row, .uir-action-secondary,
  .uir-action-row_standalone, .uir-dismiss, .uir-card-actions,
  .uir-confirm-overlay, .uir-overlay-backdrop, form button[type="submit"] {
    display: none !important;
  }
  body { background: #fff; color: #000; }
  .uir-card, .uir-mini-card {
    border: 1px solid #000;
    box-shadow: none;
    break-inside: avoid;
  }
}

.uir-tab { display: inline-flex; align-items: center; gap: .45rem; }
.uir-icon { width: 1.1rem; height: 1.1rem; display: inline-block; position: relative; color: currentColor; }
.uir-icon::before { content: ""; position: absolute; inset: .12rem; border: 2px solid currentColor; border-radius: .2rem; }
.uir-icon-list::after { content: ""; position: absolute; left: .28rem; right: .28rem; top: .3rem; height: 2px; background: currentColor; box-shadow: 0 .22rem 0 currentColor, 0 .44rem 0 currentColor; }
.uir-icon-grid::before { border-radius: .2rem; }
.uir-icon-package::before { transform: rotate(30deg); }
.uir-icon-chart::before { inset: .3rem .1rem .1rem; border-width: 0 2px 2px; border-radius: 0; }
.tenant-app { margin: 0; color: var(--uir-fg); background: var(--uir-bg); }
/* U1-S4: the product host's sign-in page — the one page this host
 * serves that the renderer does not build, so it takes its type from
 * the same resolved profile by hand rather than inheriting `#app`. */
.tenant-signin { max-width: 26rem; margin: 3rem auto; padding: 0 1rem; font-family: var(--uir-font); font-size: var(--uir-type-body-size); line-height: var(--uir-type-body-line); color: var(--uir-fg); }
.tenant-signin > p { text-align: center; font-weight: 600; margin: 0 0 var(--uir-space-md); }
.tenant-signin .uir-form > div { margin-bottom: var(--uir-space-sm); }
.tenant-app-head { display: flex; align-items: center; justify-content: space-between; gap: var(--uir-space-md); padding: var(--uir-space-md) clamp(1rem, 4vw, 3rem); color: var(--uir-on-primary); background: var(--uir-primary); }
.tenant-app-head h1 { margin: 0; font: inherit; font-weight: 700; }
.tenant-app-head a, .tenant-app-head button { color: inherit; font: inherit; }
.tenant-app-head form { margin: 0; }
.tenant-app-head button { border: 1px solid currentColor; border-radius: var(--uir-radius-sm); padding: .35rem .65rem; background: transparent; cursor: pointer; }
/* U1-S5: the surface strip, on the PRODUCT host. The same markup is
 * styled in `studio.css:179` for the Studio's review page, but this
 * host never mounts that stylesheet — it serves `/apps/assets/uir.css`
 * and nothing else — so the strip arrived here as raw default links
 * run together with no gaps. These rules are the product-host half,
 * scoped to `.tenant-app` so the Studio's copy is untouched.
 *
 * A row of chips directly under the header, sharing the header's
 * horizontal inset so the first chip lines up with the app title
 * above it. It does not wrap: a chooser that reflows into a second
 * line pushes the app itself down the page as surfaces are added.
 * It scrolls sideways instead, with the scrollbar hidden the same way
 * the bottom-navigation tab strip hides its own (`:1956`) — the edge
 * of a clipped chip is the affordance. */
.tenant-app .surface-strip {
  display: flex;
  align-items: center;
  gap: var(--uir-space-sm);
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  padding: var(--uir-space-sm) clamp(1rem, 4vw, 3rem);
  background: var(--uir-card);
  border-bottom: 1px solid var(--uir-border);
  font-size: .9rem;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
.tenant-app .surface-strip::-webkit-scrollbar { display: none; }
.tenant-app .surface-strip a {
  white-space: nowrap;
  text-decoration: none;
  padding: .35rem .8rem;
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  color: var(--uir-fg);
}
/* The open view is FILLED, not merely underlined: at this size a
 * weight change is the difference between two chips that look alike. */
.tenant-app .surface-strip a[aria-current="page"] {
  color: var(--uir-on-primary);
  background: var(--uir-primary);
  border-color: var(--uir-primary);
  font-weight: 600;
}
/* `.muted` is the markup's own word for "not the open one". It takes
 * the muted INK token rather than an opacity: opacity fades the
 * border and the background with the text and loses the chip. */
.tenant-app .surface-strip a.muted { color: var(--uir-muted); }
.tenant-app .surface-strip a.muted:hover { color: var(--uir-fg); background: var(--uir-tint); }
.tenant-app .surface-strip a:focus-visible {
  /* The same focus signal the rest of this file uses (`.uir-back`,
   * the form controls) — one token, not a second focus colour. */
  outline: 2px solid var(--uir-primary);
  outline-offset: 2px;
}
.tenant-app > main { max-width: 90rem; margin: 0 auto; }

/* The Design Package selects shell intent; these rules only materialize it. */
.uir-shell { background: var(--uir-bg); min-height: 32rem; }
/* Task Z10: `.uir-shell-body` and `.uir-document` were declared here
 * too and are deleted with the navigation rules that selected them —
 * this adapter builds `.uir-layout` / `.uir-region-*`, never those. */
.uir-header,
.uir-footer {
  display: flex;
  align-items: center;
  gap: var(--uir-item-gap);
  padding: var(--uir-shell-inset);
  background: var(--uir-card);
}
.uir-header { justify-content: space-between; border-bottom: 1px solid var(--uir-border); }
.uir-header-fixed { position: sticky; top: 0; z-index: 2; }
.uir-header-identity,
.uir-header-utilities { display: flex; align-items: center; gap: var(--uir-item-gap); min-width: 0; }
.uir-header-identity { flex-direction: column; align-items: flex-start; }
.uir-header-subtitle { color: var(--uir-muted); font-size: .85rem; }
.uir-shell-select,
.uir-navigation-toggle {
  font: inherit;
  color: var(--uir-fg);
  background: var(--uir-card);
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  padding: .35rem .5rem;
}
.uir-footer { flex-wrap: wrap; color: var(--uir-muted); border-top: 1px solid var(--uir-border); font-size: .85rem; }
/* ---------------------------------------------------------------
 * Task Z10 — side and persistent navigation, on the DOM the adapter
 * builds.
 *
 * These rules were written against a shell the adapter never built.
 * The two-column grid selected `.uir-shell-body`, and the bottom form's
 * ordering selected `.uir-document`; neither element exists, so with
 * `.uir-navigation-*` finally being written (Task Z4) the block
 * HALF-fired: `.uir-navigation-* .uir-tabs` did turn the strip into a
 * vertical list, while the grid that would have put it BESIDE the
 * content matched nothing — so at 1280px the list stacked full-width
 * ABOVE the content, worse than the top strip it replaced. The dead
 * selectors are deleted rather than left inert; a rule that matches
 * nothing "passes" every check that only asks whether a rule exists.
 *
 * The real DOM is `#app` with three SIBLING children in order:
 * `nav.uir-tabs`, the optional `.uir-chrome-bar`, and
 * `div.uir-layout.uir-shell-<kind>`. So `#app` ITSELF is the grid: the
 * nav takes column 1 and spans every row, everything else goes to
 * column 2. `align-items: start` keeps the content at the top rather
 * than stretching it to the sidebar's height.
 * --------------------------------------------------------------- */
#app.uir-navigation-side_navigation,
#app.uir-navigation-persistent_sidebar {
  display: grid;
  grid-template-columns: var(--uir-sidebar-width) minmax(0, 1fr);
  align-items: start;
}
#app.uir-navigation-side_navigation[data-sidebar-collapsed="true"],
#app.uir-navigation-persistent_sidebar[data-sidebar-collapsed="true"] {
  grid-template-columns: minmax(3.5rem, 4.5rem) minmax(0, 1fr);
}
/* Collapsed = icons only. On the SAME element as the grid above, so
 * one attribute drives both halves and they cannot disagree. */
#app.uir-navigation-side_navigation[data-sidebar-collapsed="true"] .uir-tab-label,
#app.uir-navigation-persistent_sidebar[data-sidebar-collapsed="true"] .uir-tab-label {
  display: none;
}
#app.uir-navigation-side_navigation > .uir-tabs,
#app.uir-navigation-persistent_sidebar > .uir-tabs {
  grid-column: 1;
  /* Task Z10b: `span 64`, NOT `1 / -1`.
   *
   * `-1` is the last line of the EXPLICIT grid, and `#app` declares no
   * `grid-template-rows` — so `-1` resolved to line 2 and the nav
   * spanned row 1 alone. The chrome row (forced to column 2) filled
   * row 1 beside it, and `.uir-layout`, auto-placed, landed in ROW 2:
   * the content started below the sidebar's bottom edge instead of
   * beside its top (measured at 1280px: nav top y≈147, layout top
   * y≈670).
   *
   * A count large enough to cover every column-2 child is what stays
   * correct as children are added. The alternative — declaring
   * `grid-template-rows: auto minmax(0, 1fr)` and placing the chrome
   * row and the layout in rows 1 and 2 by hand — fixes today's two
   * children and RE-BREAKS on the third (landing meta, a footer): a
   * third child auto-places into implicit row 3, which a `1 / -1` nav
   * still would not cover, and the content drops below the sidebar
   * again. Implicit rows cost nothing here: unused ones are auto-sized
   * to zero. */
  grid-row: 1 / span 64;
  /* Sticky, not fixed: fixed would take it out of the grid it is a
   * column of, and the column would collapse. */
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
  gap: var(--uir-item-gap);
  margin: 0;
  padding: var(--uir-shell-inset);
  border: 0;
  border-right: 1px solid var(--uir-border);
  background: var(--uir-card);
}
/* Everything that is not the sidebar shares column 2, in source
 * order — the chrome row and the layout, unchanged in every other
 * respect. */
#app.uir-navigation-side_navigation > *:not(.uir-tabs),
#app.uir-navigation-persistent_sidebar > *:not(.uir-tabs) {
  grid-column: 2;
  min-width: 0;
}
#app.uir-navigation-side_navigation .uir-tab,
#app.uir-navigation-persistent_sidebar .uir-tab {
  width: 100%;
  justify-content: flex-start;
  text-align: left;
  /* AD-R11: a sidebar destination is a FIXED-width column, so its label
   * has to wrap. `.uir-tab` at :697 sets `white-space: nowrap` — right
   * for a horizontal strip, where wrapping would double the strip's
   * height, and wrong here: at 288px the label "Catalog Version
   * Simulation & Publication Governance" took its max-content width,
   * left the column and painted over the content column's stat rows.
   *
   * `overflow-wrap: anywhere` is the half that does the work. A flex
   * item's automatic minimum size is its MIN-content width, which for
   * `normal` wrapping is the longest single word — so one long word
   * ("Publication") would still have pushed past 288px on its own.
   * `anywhere` lets the break fall inside a word when there is no
   * other place for it, and, unlike `break-word`, it is honoured when
   * min-content is computed, which is what keeps `min-width: 0` from
   * being decorative here.
   *
   * `align-items: flex-start` overrides the `center` from :1752 so the
   * icon sits on the label's FIRST line rather than floating at the
   * middle of a three-line block.
   *
   * Not touched: `[data-sidebar-collapsed="true"] .uir-tab-label` at
   * :1893 (the label is `display: none` there, so wrapping is moot),
   * the compact bottom bar at :1695 and :2034 (a different navigation
   * class, and it scrolls sideways by design rather than wrapping),
   * and the horizontal strip at :697. */
  align-items: flex-start;
  min-width: 0;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* `drawer_navigation` IS the default top strip, so it gets chrome and
 * nothing structural — no `#app` rule, deliberately. Kept because the
 * form is one of the four the renderer can resolve, and a form with no
 * rule at all is indistinguishable from a form nobody implemented. */
.uir-navigation-drawer_navigation .uir-tabs {
  padding: var(--uir-shell-inset);
  background: var(--uir-card);
}

/* ---------------------------------------------------------------
 * Task Z4 — the navigation FORM, as the DOM adapter actually builds it.
 *
 * `uir-adapter.js`'s `applyNavigationClass` puts
 * `.uir-navigation-<form>` on `#app`, whose children are, in order:
 * `nav.uir-tabs`, the optional `.uir-chrome-bar`, and
 * `div.uir-layout`.
 *
 * (Task Z10 closed this task's recorded residual: the block above no
 * longer selects `.uir-shell-body` / `.uir-document`, elements this
 * adapter does not build. Nothing inert is left behind either form.)
 *
 * `drawer_navigation` is deliberately absent here: it IS the default
 * top strip `.uir-tabs` already draws.
 * --------------------------------------------------------------- */
#app.uir-navigation-bottom_navigation {
  display: block;
  /* Room for the fixed bar, plus the device's own bottom inset (home
   * indicator / gesture bar), so the last row of content is reachable
   * rather than sitting under the bar. */
  padding-bottom: calc(4rem + env(safe-area-inset-bottom, 0px));
}
#app.uir-navigation-bottom_navigation .uir-tabs {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 5;
  margin: 0;
  flex-wrap: nowrap;
  gap: 0;
  padding: var(--uir-space-xs) var(--uir-space-xs)
    calc(var(--uir-space-xs) + env(safe-area-inset-bottom, 0px));
  border: 0;
  border-top: 1px solid var(--uir-border);
  background: var(--uir-card);
  /* Task Z9: a bar with more destinations than fit SCROLLS. Squeezing
   * them into one row is what cut labels mid-word at 390px ("ose
   * Store", "front —"): eight destinations sharing the viewport give
   * each about 45px, less than one word. An app does not get fewer
   * places because the phone is narrow, so the row keeps each item
   * legible and moves sideways instead.
   *
   * No visible scrollbar chrome: the bar is furniture, and a
   * always-on scrollbar under the labels reads as a broken layout.
   * The overflow is still discoverable — a partly-visible item at the
   * edge is the affordance, which is why the items do not stretch to
   * fill (`justify-content: flex-start`, not `space-around`). */
  overflow-x: auto;
  overflow-y: hidden;
  /* Task Z9b: FLEX-START, restated as load-bearing rather than tidy. A
   * centred (or `space-around`) flex row that overflows its container
   * starts scrolled past its own beginning and cannot be scrolled back
   * — the first item is unreachable, not merely off-screen, whatever
   * `scrollLeft` says. The first destination being visible at rest
   * depends on this line, not on `scrollActiveTabIntoView`. */
  justify-content: flex-start;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
#app.uir-navigation-bottom_navigation .uir-tabs::-webkit-scrollbar { display: none; }
#app.uir-navigation-bottom_navigation .uir-tab {
  /* Icon over a label, every destination the SAME fixed width. A fixed
   * basis rather than a share (`flex: 1 1 0`) or a floor with room to
   * grow: a share shrinks without limit as destinations are added, and
   * 45px shows part of a word. */
  flex: 0 0 4.5rem;
  min-width: 0;
  flex-direction: column;
  align-items: center;
  gap: .15rem;
  padding: .2rem .25rem;
  text-align: center;
  font-size: .7rem;
  line-height: 1.2;
  overflow: hidden;
}
#app.uir-navigation-bottom_navigation .uir-tab-label {
  /* Task Z9b: the ellipsis lives HERE, on the label's own box.
   *
   * `text-overflow` needs a block container whose own `overflow` is not
   * visible. Declaring it on the BUTTON did nothing: the button is a
   * flex container, so its title was an anonymous flex item, which
   * cannot ellipsize — it took its max-content width, centred, and
   * overflowed BOTH ends, which the button then clipped. That is the
   * "se Store & Ordering" the 390px screenshot showed: a label with its
   * first characters cut off as well as its last.
   *
   * `max-width: 100%` is what stops the label growing past its button
   * under `align-items: center` (a flex item's cross size is otherwise
   * max-content); the three properties after it truncate at the END, on
   * one line. */
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 639px) {
  .uir-header { padding: var(--uir-shell-inset); }
  .uir-header-utilities { gap: var(--uir-item-gap); }
  /* Task Z10: a sidebar is not a phone layout. Re-based onto `#app`
   * with the grid it now undoes — the old pair selected
   * `.uir-shell-body`, so on a narrow screen the two-column grid it
   * meant to cancel simply stayed. `display: block` returns the three
   * children to source order, which is the top strip. */
  #app.uir-navigation-side_navigation,
  #app.uir-navigation-persistent_sidebar { display: block; }
  #app.uir-navigation-side_navigation > .uir-tabs,
  #app.uir-navigation-persistent_sidebar > .uir-tabs {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    border-right: 0;
  }
}

/* ---------------------------------------------------------------
 * PC-18 (Phase 12 Wave C Task C3a) — the app's own chrome bar.
 *
 * A viewer's OWN locale and light/dark, sitting under the tab strip.
 * Deliberately quiet: this is the app's furniture, not its content, so
 * it uses the muted foreground and no card surface of its own. It is
 * absent entirely on a single-locale surface (`appChromeNode` returns
 * null), so nothing here needs an empty state.
 * --------------------------------------------------------------- */
.uir-chrome {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--uir-space-md);
  margin: calc(-1 * var(--uir-space-sm)) 0 var(--uir-space-lg);
  color: var(--uir-muted);
  font-size: 0.875rem;
}
.uir-chrome-group {
  display: flex;
  align-items: center;
  gap: var(--uir-space-xs);
  margin: 0;
}
.uir-chrome-label { color: var(--uir-muted); }
.uir-chrome-select {
  font: inherit;
  color: var(--uir-fg);
  background: var(--uir-card);
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  padding: var(--uir-space-xs) var(--uir-space-sm);
  cursor: pointer;
}
.uir-chrome-btn {
  appearance: none;
  font: inherit;
  color: var(--uir-muted);
  background: transparent;
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  padding: var(--uir-space-xs) var(--uir-space-sm);
  cursor: pointer;
  transition: color var(--uir-fast) var(--uir-ease),
              background var(--uir-fast) var(--uir-ease);
}
.uir-chrome-btn:hover { color: var(--uir-fg); background: var(--uir-tint); }
.uir-chrome-btn.on {
  color: var(--uir-fg);
  background: var(--uir-tint);
  font-weight: 600;
}
/* A pressed state must survive a forced-colours / high-contrast mode,
   where background tints are discarded — the border carries it. */
.uir-chrome-btn[aria-pressed="true"] { border-color: currentColor; }

@media (max-width: 639px) {
  .uir-chrome { justify-content: flex-start; }
}
 * Phase 12 Wave B Task B1 — the chart.
 *
 * Deliberately thin. The renderer decided WHAT is drawn (which rows,
 * which series, which kind); this decides only how the marks look, in
 * the design profile's own semantic tokens — no colour is named here
 * that the profile does not already own. Series colours cycle through
 * four accent steps; a fifth series repeats the first, which is honest
 * (the legend is what disambiguates) and is why the roster caps
 * nothing: a chart with five series is an authoring choice, not a
 * rendering failure.
 * --------------------------------------------------------------- */
.uir-chart { width: 100%; height: auto; display: block; }
.uir-chart-axis { stroke: var(--uir-border); stroke-width: 1; }
.uir-chart-category { fill: var(--uir-muted); font-size: 9px; }
.uir-chart-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.uir-chart-bar { rx: 1; }
.uir-chart-series-0 { color: var(--uir-primary); fill: var(--uir-primary); stroke: var(--uir-primary); }
.uir-chart-series-1 { color: var(--uir-ok-fg); fill: var(--uir-ok-fg); stroke: var(--uir-ok-fg); }
.uir-chart-series-2 { color: var(--uir-warn-fg); fill: var(--uir-warn-fg); stroke: var(--uir-warn-fg); }
.uir-chart-series-3 { color: var(--uir-danger); fill: var(--uir-danger); stroke: var(--uir-danger); }
.uir-chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--uir-item-gap);
  list-style: none;
  margin: var(--uir-item-gap) 0 0;
  padding: 0;
  font-size: 0.85em;
  color: var(--uir-muted);
}
.uir-chart-legend-item::before {
  content: "";
  display: inline-block;
  width: 0.6em;
  height: 0.6em;
  margin-right: 0.4em;
  border-radius: 2px;
  background: currentColor;
}

/* ---------------------------------------------------------------
 * Phase 12 Wave B Task B2 — the line-items repeating group.
 *
 * What it replaces is a `<textarea>` of raw JSON, so the bar it has to
 * clear is low; what matters is that a row READS as a row. Columns
 * share the table's own widths, the remove control sits at the end of
 * its row rather than floating, and the add control is plainly outside
 * the rows so it is never mistaken for one.
 * --------------------------------------------------------------- */
.uir-line-items { display: grid; gap: var(--uir-item-gap); }
.uir-line-items-table { width: 100%; border-collapse: collapse; }
.uir-line-items-table th {
  text-align: left;
  font-size: 0.85em;
  font-weight: 600;
  color: var(--uir-muted);
  padding: 0 var(--uir-space-sm) var(--uir-space-sm) 0;
}
.uir-line-items-table td { padding: 0 var(--uir-space-sm) var(--uir-space-sm) 0; }
.uir-line-items-table td > input,
.uir-line-items-table td > textarea { width: 100%; box-sizing: border-box; }
.uir-line-items-actions { width: 1%; white-space: nowrap; }
.uir-line-items-add { justify-self: start; }

/* ---------------------------------------------------------------
 * Phase 12 Wave B Task B3 — the derived choice-control variants, and
 * progressive reveal.
 *
 * Each family below is presentation ONLY: the renderer decided WHICH
 * family from data facts, and the adapter chose native semantics for
 * it (a radiogroup, a `<select multiple>`, a combobox over a
 * `<datalist>`, a `<details>`). Nothing here changes what a control
 * means, and nothing here is reachable without the renderer having
 * emitted the family — so a rule that stops matching is a dead rule,
 * not a broken control.
 * --------------------------------------------------------------- */
.uir-segmented {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0;
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-md);
  overflow: hidden;
}
.uir-segmented-option {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: var(--uir-space-sm) var(--uir-space-md);
  cursor: pointer;
  border-right: 1px solid var(--uir-border);
}
.uir-segmented-option:last-child { border-right: 0; }
.uir-segmented-option:has(input:checked) {
  background: var(--uir-accent-tint);
  font-weight: 600;
}
.uir-multi-chips { min-height: 6rem; width: 100%; }
.uir-searchable,
.uir-typeahead { display: inline-flex; width: 100%; }
.uir-searchable > input,
.uir-typeahead > input { width: 100%; }
/* A control that fell back to a simpler family SAYS SO, quietly. The
 * attribute is set only when `variant_requested` differs from
 * `variant`, so this never fires on a host that draws what the data
 * asked for. */
[data-uir-variant-fallback] { border-bottom: 1px dashed var(--uir-muted); }
.uir-disclosure {
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-md);
  padding: var(--uir-space-sm) var(--uir-space-md);
}
.uir-disclosure-summary {
  cursor: pointer;
  font-weight: 600;
  padding: var(--uir-space-sm) 0;
}
.uir-disclosure .uir-field-group { border: 0; padding: 0; margin: 0; }

/* -----------------------------------------------------------------
 * SHELL-1: the frame (web-composition design §2, "Wave SHELL-1").
 *
 * The renderer decides WHICH frame and what goes in it
 * (`renderer/src/ir.rs`'s `ShellIr`); the adapter builds the boxes;
 * these rules are the only thing that says what any of it looks like.
 * The same division of labour `.uir-pattern-*` and `.uir-shell-*`
 * above already keep.
 *
 * Every colour, gap and radius here is a `--uir-*` token the adapter
 * already resolves from the Design Package. There is no literal
 * colour in this block: a tenant who re-themes their app re-themes
 * their frame for free, which is the whole reason the frame moved out
 * of the host's hand-written header.
 * ----------------------------------------------------------------- */

/* `min-width: 0` on every frame box is load-bearing, not tidying.
 * A flex item's automatic minimum size is its CONTENT's, so one
 * `white-space: nowrap` row anywhere inside can push a box wider than
 * its parent and take the page with it — which is exactly what the
 * surface chooser did at 390px (SHELL-1c finding 2). Zero lets each
 * box shrink to its share and makes the `overflow` on the scrolling
 * children mean something. */
.uir-frame { display: flex; flex-direction: column; min-width: 0; min-height: 100vh; background: var(--uir-bg); }
/* LAYOUT-1b: `overflow-x: clip`, not `auto` and not `hidden`.
 *
 * `clip` refuses the overflow without creating a scroll container, so
 * a band that tries to escape its column is CUT rather than given a
 * sideways scrollbar — and, unlike `hidden`, it does not make this box
 * a scroll port, which would break `position: sticky` on the frame's
 * own header and on the filter bar inside it.
 *
 * The backstop, never the fix: the hero's own rule is arithmetic-free
 * now (see `.uir-region-hero`). This is here so the NEXT full-bleed
 * idea cannot put a horizontal scrollbar on a tenant's front door
 * before anyone measures it. */
.uir-frame-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: clip;
}

/* --- the brand, shared by both frames ---------------------------- */
.uir-brand {
  display: flex;
  align-items: center;
  gap: var(--uir-space-sm);
  min-width: 0;
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--uir-primary);
}
.uir-brand-mark {
  width: 1.35rem;
  height: 1.35rem;
  flex: 0 0 auto;
  border-radius: var(--uir-radius-sm);
  background: var(--uir-primary);
}
/* The name TRUNCATES rather than wraps. A two-line brand pushes the
 * whole rail or row down and the app with it. */
.uir-brand-name,
.uir-brand > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- the storefront header: brand, nav row, cart ------------------ */
.uir-topnav {
  display: flex;
  align-items: center;
  gap: var(--uir-space-lg);
  padding: var(--uir-space-md) clamp(1rem, 4vw, 3rem);
  background: var(--uir-card);
  border-bottom: 1px solid var(--uir-border);
}
.uir-topnav > .uir-brand { flex: 0 0 auto; max-width: 14rem; font-size: 1.05rem; }

/* --- the dashboard header: back, title, utilities ----------------- */
.uir-topbar {
  display: flex;
  align-items: center;
  gap: var(--uir-space-sm);
  padding: var(--uir-space-sm) var(--uir-shell-inset);
  background: var(--uir-card);
  border-bottom: 1px solid var(--uir-border);
}
.uir-topbar-title { margin: 0; font-size: 1rem; font-weight: 600; letter-spacing: -.015em; }

/* The access affordance (sign out / staff sign-in) is pushed to the
 * END of the TITLE BAR, which carries it as a direct child.
 * `margin-inline-start: auto` rather than a spacer element: the gap is
 * presentation, and a spacer would be a node a screen reader walks
 * through.
 *
 * The NAV-ROW header is not here: its access node lives inside
 * `.uir-topnav-utilities`, which does the pushing for the whole
 * cluster. Selecting it here too would push it away from the cart it
 * is meant to sit beside. */
.uir-topbar > .uir-access,
.uir-topbar > .uir-access-link { margin-inline-start: auto; flex: 0 0 auto; }
.uir-topnav-utilities > .uir-access,
.uir-topnav-utilities > .uir-access-link { flex: 0 0 auto; }
.uir-access { margin: 0; }
.uir-access-btn {
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  padding: .35rem .65rem;
  background: transparent;
  color: var(--uir-muted);
  font: inherit;
  cursor: pointer;
}
.uir-access-btn:hover { color: var(--uir-fg); background: var(--uir-tint); }
.uir-access-link { color: var(--uir-muted); font-size: .9rem; text-decoration: none; }
.uir-access-link:hover { color: var(--uir-fg); text-decoration: underline; }

/* --- the nav row, and the fold ------------------------------------
 *
 * ONE LINE, always. It takes what is left between the brand and the
 * cart and CLIPS rather than wraps: a slow measurement must never
 * show two lines and then snap back to one.
 *
 * The row itself clips nothing — a clip on either axis would take the
 * "More" menu with it, however the menu is positioned, and a clipped
 * menu cannot be clicked. The strip of links inside does the clipping.
 *
 * `gap: 4px` is mirrored by `NAV_GAP` in `uir-adapter.js`. The two
 * have to agree or the fold is computed against a row nobody drew;
 * `uir-shell-frame.test.mjs` pins them together. */
.uir-navrow {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1 1 auto;
  min-width: 0;
  flex-wrap: nowrap;
}
.uir-navrow-links {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 0 1 auto;
  min-width: 0;
  flex-wrap: nowrap;
  /* Sideways only: `hidden` would force the other axis to scroll. */
  overflow-x: clip;
  overflow-y: visible;
}
/* Links keep their natural width, so measuring one tells the truth. */
.uir-navrow-links > button,
.uir-navrow > .uir-navmore { flex: 0 0 auto; }
.uir-navlink {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  white-space: nowrap;
  border: 0;
  border-radius: var(--uir-radius-sm);
  padding: .4rem .7rem;
  font: inherit;
  font-size: .9rem;
  color: var(--uir-muted);
  background: transparent;
  cursor: pointer;
}
.uir-navlink:hover { color: var(--uir-fg); background: var(--uir-tint); }
.uir-navlink.on {
  color: var(--uir-primary);
  background: var(--uir-accent-tint);
  font-weight: 600;
}
.uir-navlink:focus-visible { outline: 2px solid var(--uir-primary); outline-offset: 2px; }
.uir-navlink-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

.uir-navmore { position: relative; flex: 0 0 auto; }
.uir-navmore-menu {
  /* Above the page, not under it: the title band below is a later
   * sibling, so the menu needs its own layer to be reachable at all.
   * It hangs from the row's end leftwards, which keeps it in frame. */
  position: absolute;
  top: calc(100% + 5px);
  inset-inline-end: 0;
  z-index: 30;
  /* As wide as its longest label, and never wider than a menu should
   * be. A label past the cap ellipsizes rather than spilling. */
  width: max-content;
  min-width: 10.5rem;
  max-width: 17.5rem;
  display: flex;
  flex-direction: column;
  padding: 4px;
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-md);
  background: var(--uir-card);
  box-shadow: var(--uir-shadow-md);
}
.uir-navmore-menu[hidden] { display: none; }
.uir-navmore-item {
  border: 0;
  background: transparent;
  border-radius: var(--uir-radius-sm);
  padding: .4rem .6rem;
  text-align: start;
  font: inherit;
  font-size: .9rem;
  white-space: nowrap;
  color: var(--uir-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
.uir-navmore-item:hover { color: var(--uir-fg); background: var(--uir-tint); }
.uir-navmore-item.on { color: var(--uir-primary); font-weight: 600; }

/* --- the header's right-hand cluster ------------------------------- */
/* Menu (on a phone), Cart, and the access affordance, as ONE flex item
 * rather than three loose siblings. Three siblings each claiming the
 * same free space is what let the Menu button draw under the Cart pill
 * at 390px — see `buildShellFrame`'s `topNavUtilities` for the full
 * account. `flex-wrap` is the honest overflow behaviour here: if the
 * words genuinely do not fit on one line they take a second, which is
 * visible, rather than sliding over each other, which is not. */
.uir-topnav-utilities {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--uir-space-sm);
  flex: 0 1 auto;
  flex-wrap: wrap;
  margin-inline-start: auto;
}

/* --- compact: the storefront nav folds to a drawer ----------------- */
/* `flex: 0 0 auto`, NOT `1 1 auto` with `min-width: 0`. This wrapper
 * holds exactly one button; letting it shrink below that button does
 * not shrink the button, it just makes the button overflow its own
 * box and land on whatever comes next. That was the 390px overlap. */
.uir-navdrawer { position: relative; flex: 0 0 auto; }
.uir-navdrawer-btn {
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-sm);
  padding: .4rem .7rem;
  background: transparent;
  color: var(--uir-fg);
  font: inherit;
  cursor: pointer;
}
.uir-navdrawer-panel {
  position: absolute;
  top: calc(100% + 6px);
  inset-inline-start: 0;
  z-index: 30;
  min-width: 12rem;
  display: flex;
  flex-direction: column;
  padding: 4px;
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius-md);
  background: var(--uir-card);
  box-shadow: var(--uir-shadow-md);
}
.uir-navdrawer-panel[hidden] { display: none; }

/* --- the cart pill ------------------------------------------------- */
.uir-cart {
  /* No `margin-inline-start: auto` any more: the pill sits inside
   * `.uir-topnav-utilities`, which carries the push-to-the-end for the
   * whole cluster. An auto margin here would push the pill away from
   * its own siblings inside that box. */
  display: inline-flex;
  align-items: center;
  gap: var(--uir-space-sm);
  flex: 0 0 auto;
  white-space: nowrap;
  border: 0;
  border-radius: 999px;
  padding: .4rem .85rem;
  font: inherit;
  font-size: .85rem;
  font-weight: 500;
  color: var(--uir-primary);
  background: var(--uir-accent-tint);
  cursor: pointer;
}
/* The pill is a plain span until a `collect` screen exists to open
 * (LAYOUT-1). It still says the app has a cart, which is true. */
span.uir-cart { cursor: default; }
/* The cart-plus-access sibling rule that used to live here is gone:
 * both now sit inside `.uir-topnav-utilities`, whose own `gap` spaces
 * them and whose `margin-inline-start: auto` pushes the pair. One box
 * with a gap says what two sibling selectors were approximating. */

/* --- the surface switch -------------------------------------------- */
/* POLISH-1g: the chooser is no longer a BAR.
 *
 * `.uir-surfaces` (a full-width chip row under the header, on every
 * page) is gone from this file with the code that drew it. On a
 * storefront the OTHER surface is now one trailing link in the
 * header's utility cluster; on a dashboard it is the rail's
 * "Workspace" group, which it already was. The board pass measured
 * the old bar on 20 straight pages, always saying the same two words,
 * one of which was always where you already are. */
.uir-surface-switch {
  white-space: nowrap;
  text-decoration: none;
  padding: .3rem .75rem;
  border: 1px solid var(--uir-border);
  border-radius: 999px;
  color: var(--uir-muted);
  font-size: .875rem;
}
.uir-surface-switch:hover { color: var(--uir-fg); background: var(--uir-tint); }
.uir-surface-switch:focus-visible { outline: 2px solid var(--uir-primary); outline-offset: 2px; }
/* Inside the phone's Menu drawer it is a drawer row, not a pill —
 * the same shape the nav links beside it take. */
.uir-navdrawer-panel .uir-surface-switch {
  display: block;
  border: 0;
  border-radius: 0;
  padding: var(--uir-space-sm) var(--uir-space-md);
}

/* --- the title band ------------------------------------------------ */
.uir-titleband {
  display: flex;
  align-items: center;
  gap: var(--uir-space-md);
  padding: var(--uir-space-lg) clamp(1rem, 4vw, 3rem) .3rem;
}
.uir-screen-title { margin: 0; font-size: 1.4rem; font-weight: 600; letter-spacing: -.02em; }
.uir-detailnav { margin-inline-start: auto; display: flex; flex-wrap: wrap; gap: 4px; }
.uir-detailchip {
  white-space: nowrap;
  border: 1px solid var(--uir-border);
  border-radius: 999px;
  padding: .2rem .7rem;
  font: inherit;
  font-size: .78rem;
  color: var(--uir-muted);
  background: var(--uir-card);
  cursor: pointer;
}
.uir-detailchip.on {
  color: var(--uir-primary);
  border-color: var(--uir-primary);
  background: var(--uir-accent-tint);
}
.uir-back-bar,
.uir-back-band { flex: 0 0 auto; }

/* --- the page-tab strip (filled by LAYOUT-1) ---------------------- */
.uir-pagetabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: var(--uir-space-sm) clamp(1rem, 4vw, 3rem) 0;
  background: var(--uir-card);
  border-bottom: 1px solid var(--uir-border);
}
.uir-pagetab {
  margin-bottom: -1px;
  white-space: nowrap;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: .45rem .75rem;
  background: transparent;
  font: inherit;
  font-size: .85rem;
  color: var(--uir-muted);
  cursor: pointer;
}
.uir-pagetab.on { color: var(--uir-primary); border-bottom-color: var(--uir-primary); font-weight: 500; }

/* --- the storefront footer ---------------------------------------- */
.uir-storefoot {
  margin-top: auto;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: 2rem;
  padding: var(--uir-space-lg) clamp(1rem, 4vw, 3rem);
  background: var(--uir-card);
  border-top: 1px solid var(--uir-border);
  font-size: .8rem;
  color: var(--uir-muted);
}
.uir-storefoot-col { display: flex; flex-direction: column; gap: .25rem; }
.uir-storefoot-name { font-size: .9rem; font-weight: 600; color: var(--uir-fg); }
.uir-storefoot-head { font-weight: 500; color: var(--uir-fg); }

/* --- the dashboard rail -------------------------------------------- */
.uir-frame-dashboard {
  display: grid;
  grid-template-columns: var(--uir-sidebar-width) 1fr;
  align-items: stretch;
}
.uir-frame-column { display: flex; flex-direction: column; min-width: 0; background: var(--uir-bg); }
.uir-rail {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--uir-space-md) var(--uir-space-sm);
  background: var(--uir-card);
  border-inline-end: 1px solid var(--uir-border);
}
.uir-rail > .uir-brand { padding: .25rem .6rem var(--uir-space-md); font-size: 1rem; }
.uir-railnav { display: flex; flex-direction: column; gap: 2px; }
.uir-railnav .uir-navlink,
.uir-railgroup .uir-navlink { width: 100%; text-align: start; }
.uir-railgroup { display: flex; flex-direction: column; gap: 2px; margin-top: var(--uir-space-md); }
.uir-raillabel {
  padding: .4rem .6rem .2rem;
  font-size: .65rem;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--uir-muted);
}
.uir-navlink-sub { font-size: .8rem; padding-inline-start: 1.1rem; }
/* Stays in view while the canvas scrolls, the way a real app's
 * account chip does. */
.uir-userchip {
  margin-top: auto;
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: var(--uir-space-sm);
  padding: var(--uir-space-sm);
  border-top: 1px solid var(--uir-border);
  background: var(--uir-card);
  font-size: .78rem;
  color: var(--uir-muted);
}
.uir-userchip-mark {
  width: 1.7rem;
  height: 1.7rem;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--uir-primary);
  color: var(--uir-on-primary);
  display: grid;
  place-items: center;
  font-size: .7rem;
  font-weight: 600;
}
.uir-userchip-who { min-width: 0; display: flex; flex-direction: column; }
/* `all: unset` undoes the avatar rule above for the NAME, which is
 * also a `<b>`. Order-dependent and load-bearing. */
.uir-userchip-name { all: unset; display: block; color: var(--uir-fg); font-weight: 500; }
.uir-userchip-name,
.uir-userchip-role { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- compact: the rail becomes a bottom bar ------------------------ */
@media (max-width: 640px) {
  .uir-frame-dashboard { display: flex; flex-direction: column; }
  .uir-frame-dashboard > .uir-rail {
    position: sticky;
    bottom: 0;
    z-index: 20;
    flex-direction: row;
    align-items: center;
    gap: var(--uir-space-sm);
    overflow-x: auto;
    border-inline-end: 0;
    border-top: 1px solid var(--uir-border);
    scrollbar-width: none;
  }
  .uir-frame-dashboard > .uir-rail::-webkit-scrollbar { display: none; }
  /* A nowrap row of destinations must scroll inside its own box,
   * never widen it. */
  .uir-frame-dashboard > .uir-rail { min-width: 0; max-width: 100%; }
  /* The brand is the rail's DESKTOP furniture: on a bottom bar there
   * is room for destinations and nothing else, and the title bar
   * above already names the app.
   *
   * POLISH-1g: the record group, the workspace group and the user
   * chip are no longer hidden HERE — the frame does not put them in
   * the bar at all at compact, it puts them behind the bar's "More"
   * disclosure. These selectors were `> .uir-railgroup` /
   * `> .uir-userchip` and stopped matching the moment POLISH-1e
   * nested those nodes one box deeper, which is how a whole navy
   * block of links appeared mid-page at 390. A rule that has to know
   * today's nesting is a rule that breaks quietly; the structure is
   * the guard now, and `uir-shell-frame.test.mjs` pins it. */
  .uir-frame-dashboard > .uir-rail > .uir-brand { display: none; }
  .uir-frame-dashboard > .uir-rail .uir-railnav { flex-direction: row; gap: var(--uir-space-sm); }
  .uir-frame-dashboard > .uir-rail .uir-navlink { flex-direction: column; font-size: .7rem; }
  .uir-topnav,
  .uir-titleband,
  .uir-pagetabs,
  .uir-storefoot { padding-inline: var(--uir-shell-inset); }
  /* Brand left, cluster right, and a second row only when the words
   * genuinely will not fit on one. The brand is the part that gives:
   * it can shrink and ellipsize (`.uir-brand-name`), where the Menu,
   * Cart and access words cannot be truncated without becoming
   * guesses. */
  .uir-topnav { gap: var(--uir-space-sm); flex-wrap: wrap; }
  .uir-topnav > .uir-brand { flex: 1 1 auto; min-width: 0; max-width: none; }
  .uir-screen-title { font-size: 1.15rem; }
}

/* =====================================================================
 * POLISH-1 — the CableRUS visual acceptance list, 2026-09-12.
 *
 * Read the acceptance list first:
 * `docs/superpowers/specs/2026-09-12-cablerus-visual-acceptance.md`.
 * Its "Design plan" section is the brief every rule below answers, and
 * the id in each comment (1.1, 4.2, …) is the line it answers.
 *
 * LAST in the file on purpose. Every rule here has the same
 * specificity as the one it replaces, so cascade ORDER is what makes
 * it win — and that keeps the section readable as one design decision
 * rather than scattered through 3 000 lines of history. Nothing above
 * was deleted: an app rendered without this section still works, it
 * just looks like it did before the review.
 *
 * House rules unchanged: no literal colours except the one hairline
 * FALLBACK below (a token's floor value, not a decision) and the
 * white-over-dark tints on the rail, which have no token because the
 * rail's own background is the tenant's and the tint has to be
 * relative to it. Nothing here changes what an element MEANS.
 * ===================================================================== */

/* ---------------------------------------------------------------
 * 1.5 — Public Sans, self-hosted.
 *
 * Four weights, no italics: the design plan asks for 400/500/600/700
 * and every unused face is ~33 KB a visitor might download. The files
 * are compiled into the host binary (`apps.rs`'s `FONT_FILES`) and
 * served from the same origin as this stylesheet — never a font CDN,
 * which would leak every visitor's IP and referrer to a host the
 * tenant never chose and would fail closed on an offline laptop.
 *
 * `font-display: swap`: the fallback shows immediately and the real
 * face replaces it. `block` would give a tenant a blank page while a
 * 33 KB file arrives.
 *
 * Licence: SIL OFL 1.1, text at `static/fonts/OFL.txt`.
 * --------------------------------------------------------------- */
@font-face {
  font-family: "Public Sans";
  src: url("/apps/assets/fonts/PublicSans-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Public Sans";
  src: url("/apps/assets/fonts/PublicSans-Medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Public Sans";
  src: url("/apps/assets/fonts/PublicSans-SemiBold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Public Sans";
  src: url("/apps/assets/fonts/PublicSans-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ---------------------------------------------------------------
 * The adapter-local scales this wave adds.
 *
 * TYPE (1.5): six steps, and no seventh. The substrate profile's
 * `TypographyRole` roster has exactly two entries (`body`, `heading`)
 * because only two `typography_role` tags are emitted; a display size
 * and a label size are ADAPTER presentation, the same class as the
 * shadow scale at the top of this file, so they live here rather than
 * widening a wire roster nothing produces.
 *
 * GUTTER (1.1): one value, used by the body column AND by every band
 * of the frame, which is what makes the header gutter and the body
 * gutter the same number rather than two numbers that agree today.
 * --------------------------------------------------------------- */
:root {
  --uir-type-display: 44px;
  --uir-type-display-sm: 32px;
  --uir-type-h1: 24px;
  --uir-type-h2: 20px;
  --uir-type-h3: 18px;
  --uir-type-label: 13px;
  --uir-type-small: 14px;
  --uir-track-tight: -.02em;
  --uir-gutter: 48px;
  --uir-measure: 1200px;
  /* 1.4: the hairline. A token so a package can restate it; the
   * fallback is the design plan's own value for a package that does
   * not state a border colour at all. */
  --uir-hairline: var(--uir-border, #E3E9EF);
  /* The accent, when no design package states one. Deliberately the
   * primary rather than a second invented hue: an app whose package
   * is silent gets one colour, not two. */
  --uir-accent: var(--uir-primary);
}
[data-uir-class="compact"] {
  --uir-gutter: 16px;
  --uir-type-display: var(--uir-type-display-sm);
}

/* Body type (1.5). Sentence case is AUTHORED, not transformed: there
 * is no `text-transform: uppercase` anywhere in this section, and the
 * ones that existed above it are undone in the label rule below. */
#app {
  font-size: 16px;
  line-height: 1.5;
  font-weight: 400;
}
/* 1.5: tabular numerals for money and counts. `#app` already sets
 * `font-variant-numeric` globally; these are the places where the
 * figure is the point and a fallback face must not undo it. */
.uir-format-money,
.uir-stat-value,
.uir-pricing-amount,
.uir-cart-count { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

/* ---------------------------------------------------------------
 * 1.1 / 1.2 — the centred content column.
 *
 * ONE technique, applied to every band of both frames: symmetric
 * padding of `max(gutter, (100% - measure) / 2)`. Below 1200 + 2×48
 * the max resolves to the gutter and the band is inset by exactly the
 * gutter; above it the extra space splits evenly and the content
 * stops growing at 1200px. The frame's own background fills the rest,
 * because the padding is INSIDE the band rather than a margin on it.
 *
 * Why not a wrapper element per band: a wrapper is a node in the
 * accessibility tree that means nothing, it has to be added to six
 * builders that do not otherwise resemble each other, and the
 * full-bleed hero would then have to break OUT of it — which is the
 * negative-margin arithmetic LAYOUT-1b removed. Padding keeps the
 * band full-bleed and its CONTENT aligned, which is exactly the
 * relationship the design asks for.
 * --------------------------------------------------------------- */
.uir-topnav,
.uir-topbar,
.uir-titleband,
.uir-pagetabs,
.uir-storefoot,
.uir-frame-body > .uir-layout {
  padding-inline: max(var(--uir-gutter), (100% - var(--uir-measure)) / 2);
  box-sizing: border-box;
}
/* The dashboard's column sits beside a rail, so its own 100% is
 * already the narrower box; the same expression still centres its
 * content and still stops at the measure. */
.uir-frame-dashboard > .uir-frame-column { min-width: 0; }
.uir-frame-body > .uir-layout { padding-block: var(--uir-space-xl); }
/* 1.9: nothing may push the page wider than the viewport. The frame
 * already clips; this stops the two most common causes from arising —
 * a grid cell whose content has a min-content wider than its share,
 * and a table that refuses to shrink. */
.uir-grid-cell,
.uir-region-main,
.uir-region-side { min-width: 0; }

/* ---------------------------------------------------------------
 * 1.3 — the rhythm. One scale, three uses.
 * --------------------------------------------------------------- */
.uir-grid { gap: var(--uir-space-lg); }
.uir-grid-row { gap: var(--uir-space-lg); }
.uir-card { padding: var(--uir-space-lg); }
.uir-field { margin-block: 0; }
form.uir-form .uir-field + .uir-field { margin-block-start: var(--uir-space-md); }

/* ---------------------------------------------------------------
 * 1.4 — surfaces. 10px radius, a hairline, and ONE shadow used only
 * under things that float.
 *
 * The design plan's own sentence: "cards only where content is a
 * card (plans, dialogs), never for lists". A table inside a raised
 * box reads as a component; a table on the page reads as the page's
 * data. So `.uir-card` loses its shadow here and keeps its border,
 * and the three things that genuinely float keep the shadow.
 * --------------------------------------------------------------- */
.uir-card {
  border: 1px solid var(--uir-hairline);
  border-radius: var(--uir-radius-md);
  box-shadow: none;
  background: var(--uir-card);
}
.uir-mini-card {
  border: 1px solid var(--uir-hairline);
  border-radius: var(--uir-radius-md);
  box-shadow: none;
}
.uir-dialog,
.uir-drawer,
.uir-pricing-card { box-shadow: var(--uir-shadow-sm); }

/* ---------------------------------------------------------------
 * 1.5 — the type scale in use. One weight per role, sentence case,
 * and NO all-caps: the label rule below sets `text-transform: none`
 * explicitly, so an uppercase rule earlier in the file is undone here
 * rather than edited in place and this section stays the whole story.
 * --------------------------------------------------------------- */
.uir-screen-title,
.uir-topbar-title { font-size: var(--uir-type-h1); font-weight: 600; letter-spacing: var(--uir-track-tight); }
.uir-card > h2,
.uir-section > h2 { font-size: var(--uir-type-h2); font-weight: 600; margin-block: 0 var(--uir-space-md); }
.uir-card-title,
.uir-detail-section-title { font-size: var(--uir-type-h3); font-weight: 600; margin: 0 0 var(--uir-space-sm); }
.uir-kv-label,
.uir-field-label,
.uir-stat-label,
.uir-raillabel,
.uir-narrow-sortlabel,
.uir-badge-label,
.uir-th {
  font-size: var(--uir-type-label);
  font-weight: 500;
  color: var(--uir-muted);
  text-transform: none;
  letter-spacing: 0;
}
.uir-empty,
.uir-muted { font-size: var(--uir-type-small); color: var(--uir-muted); }

/* ---------------------------------------------------------------
 * 1.7 — the title band. One title, one Back, one status pill.
 * --------------------------------------------------------------- */
.uir-titleband {
  display: flex;
  align-items: center;
  gap: var(--uir-space-md);
  flex-wrap: wrap;
  padding-block: var(--uir-space-lg) 0;
}
.uir-statuspill {
  display: inline-flex;
  align-items: center;
  border-radius: var(--uir-radius-full);
  padding: .2rem .7rem;
  font-size: var(--uir-type-label);
  font-weight: 500;
}
/* The chevron, as presentation — never a character in the label, so
 * it stays out of the accessible name and out of the font's job. */
.uir-back {
  background: transparent;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--uir-muted);
  cursor: pointer;
}
.uir-back::before { content: "\2039"; margin-inline-end: .35rem; }
.uir-back:hover { color: var(--uir-fg); }

/* ---------------------------------------------------------------
 * 3.2 — the title bar's right-hand side.
 * --------------------------------------------------------------- */
.uir-topbar { padding-block: var(--uir-space-md); gap: var(--uir-space-md); }
.uir-topbar-utilities {
  display: flex;
  align-items: center;
  gap: var(--uir-space-sm);
  margin-inline-start: auto;
  min-width: 0;
}
.uir-narrowing-bar { display: flex; gap: var(--uir-space-sm); margin: 0; }
.uir-narrowing-bar .uir-narrow-search { min-width: 14rem; }

/* ---------------------------------------------------------------
 * 3.1 — the staff rail: Deep water, light text, Signal for active.
 *
 * The design plan's reason, kept here because it is the only thing
 * that makes a dark rail a decision rather than a fashion: staff
 * tooling should read as a different ROOM from the shop. The rail
 * colours are the design package's own (`surface_sidebar`,
 * `text_sidebar`, `surface_sidebar_selected`), which had no CSS
 * producer at all before POLISH-1 — see `PACKAGE_COLOR_VARS` in
 * `renderer/src/profile.rs`.
 * --------------------------------------------------------------- */
.uir-rail {
  background: var(--uir-rail-bg, var(--uir-card));
  color: var(--uir-rail-fg, var(--uir-fg));
  border-inline-end: 0;
  padding: var(--uir-space-lg) var(--uir-space-md);
  gap: var(--uir-space-lg);
}
.uir-rail .uir-brand,
.uir-rail .uir-brand-name { color: var(--uir-rail-fg, var(--uir-fg)); }
.uir-rail .uir-brand-mark { background: var(--uir-rail-active, var(--uir-primary)); }
.uir-rail .uir-raillabel { color: var(--uir-rail-muted, var(--uir-muted)); }
.uir-rail .uir-navlink {
  color: var(--uir-rail-muted, var(--uir-muted));
  border-radius: var(--uir-radius-sm);
}
.uir-rail .uir-navlink:hover { color: var(--uir-rail-fg, var(--uir-fg)); background: rgb(255 255 255 / 8%); }
.uir-rail .uir-navlink.on {
  color: var(--uir-rail-fg, var(--uir-fg));
  background: rgb(255 255 255 / 10%);
  box-shadow: inset 3px 0 0 var(--uir-rail-active, var(--uir-primary));
}
.uir-railgroup-workspace { margin-block-start: var(--uir-space-md); }
/* The user chip, pinned to the bottom, with its menu above it. */
.uir-userchip-wrap { margin-block-start: auto; position: relative; display: flex; flex-direction: column; }
.uir-rail .uir-userchip {
  display: flex;
  align-items: center;
  gap: var(--uir-space-sm);
  width: 100%;
  background: transparent;
  border: 0;
  border-radius: var(--uir-radius-sm);
  padding: var(--uir-space-sm);
  font: inherit;
  text-align: start;
  color: var(--uir-rail-fg, var(--uir-fg));
  cursor: pointer;
}
.uir-rail .uir-userchip[aria-disabled="true"] { cursor: default; }
.uir-rail .uir-userchip:hover { background: rgb(255 255 255 / 8%); }
.uir-userchip-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  flex: 0 0 auto;
  border-radius: var(--uir-radius-full);
  background: var(--uir-rail-active, var(--uir-primary));
  color: var(--uir-rail-bg, var(--uir-on-primary));
  font-size: var(--uir-type-label);
  font-weight: 600;
}
.uir-userchip-who { display: flex; flex-direction: column; min-width: 0; }
.uir-userchip-name { font-size: var(--uir-type-small); font-weight: 600; }
.uir-userchip-role { font-size: var(--uir-type-label); color: var(--uir-rail-muted, var(--uir-muted)); }
.uir-usermenu {
  order: -1;
  margin-block-end: var(--uir-space-xs);
  border-radius: var(--uir-radius-md);
  background: var(--uir-card);
  border: 1px solid var(--uir-hairline);
  box-shadow: var(--uir-shadow-sm);
  padding: var(--uir-space-xs);
}
.uir-usermenu .uir-access-btn,
.uir-usermenu .uir-access-link { display: block; width: 100%; border: 0; text-align: start; }

/* ---------------------------------------------------------------
 * 2.2 — the hero. The page's ONE bold element.
 *
 * A Deep-water band, the headline at 44/700 on the left, the photo on
 * the right, and the photo's lower edge hanging BELOW the band. The
 * overlap is the whole gesture: it is what makes the band read as a
 * composed thing rather than a coloured rectangle, and it is why the
 * hero region carries bottom padding — the picture needs somewhere to
 * hang into.
 *
 * The overlap is a `translate`, never a negative margin: a translate
 * takes the element out of layout flow for painting only, so nothing
 * below it moves and nothing can be pushed past the viewport edge
 * (1.9). The band is `overflow: visible` for the same reason a clip
 * would defeat the gesture.
 * --------------------------------------------------------------- */
.uir-region-hero:not(:empty) { padding-block-end: 3rem; }
.uir-hero {
  background: var(--uir-primary);
  color: var(--uir-on-primary);
  border-radius: var(--uir-radius-md);
  border: 0;
  padding: clamp(2rem, 5vw, 3.5rem) var(--uir-space-xl);
  overflow: visible;
  display: grid;
  gap: var(--uir-space-xl);
  align-items: center;
}
.uir-hero-words { display: flex; flex-direction: column; gap: var(--uir-space-md); align-items: flex-start; min-width: 0; }
.uir-hero-headline {
  margin: 0;
  font-size: var(--uir-type-display);
  line-height: 1.1;
  font-weight: 700;
  letter-spacing: var(--uir-track-tight);
  max-width: 18ch;
}
.uir-hero-body {
  margin: 0;
  color: var(--uir-on-primary);
  opacity: .82;
  font-size: var(--uir-type-h3);
  max-width: 46ch;
}
/* One white button with Signal text — the inverse of the page's
 * primary button, because a Deep-water button on a Deep-water band
 * would disappear. */
.uir-hero .uir-action-primary,
.uir-hero-cta {
  background: var(--uir-on-primary);
  color: var(--uir-accent);
  border: 0;
  border-radius: var(--uir-radius-sm);
  padding: .7rem 1.25rem;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.uir-hero .uir-action-primary:hover,
.uir-hero-cta:hover { background: var(--uir-bg); }
.uir-hero-media { min-width: 0; }
.uir-hero-image {
  display: block;
  width: 100%;
  height: 100%;
  max-height: 22rem;
  object-fit: cover;
  border-radius: var(--uir-radius-md);
  box-shadow: var(--uir-shadow-sm);
}
.uir-hero-image-alt { margin: 0; color: var(--uir-on-primary); opacity: .82; }
/* 1280: text 7/12, photo 5/12, and the photo hangs below the band. */
[data-uir-class="expanded"] .uir-hero-split,
[data-uir-class="medium"] .uir-hero-split { grid-template-columns: 7fr 5fr; }
[data-uir-class="expanded"] .uir-hero-split .uir-hero-media { transform: translateY(2.5rem); }
/* 390: the photo sits ABOVE the text, at a 16:9 crop, and does not
 * hang — an overlap on a phone is a picture half off the screen. */
[data-uir-class="compact"] .uir-hero { padding: var(--uir-space-lg); gap: var(--uir-space-lg); }
[data-uir-class="compact"] .uir-hero-split { grid-template-columns: 1fr; }
[data-uir-class="compact"] .uir-hero-split .uir-hero-media { order: -1; transform: none; }
[data-uir-class="compact"] .uir-hero-image { aspect-ratio: 16 / 9; max-height: none; }
[data-uir-class="compact"] .uir-region-hero:not(:empty) { padding-block-end: 0; }

/* ---------------------------------------------------------------
 * 2.3 / 2.4 — the plan cards.
 * --------------------------------------------------------------- */
.uir-preset-pricing { display: grid; gap: var(--uir-space-lg); grid-template-columns: repeat(3, 1fr); }
[data-uir-class="medium"] .uir-preset-pricing { grid-template-columns: repeat(2, 1fr); }
[data-uir-class="compact"] .uir-preset-pricing { grid-template-columns: 1fr; }
.uir-pricing-card {
  display: flex;
  flex-direction: column;
  gap: var(--uir-space-sm);
  padding: var(--uir-space-lg);
  background: var(--uir-card);
  text-align: start;
}
.uir-pricing-tag {
  align-self: flex-start;
  border-radius: var(--uir-radius-full);
  padding: .15rem .6rem;
  font-size: var(--uir-type-label);
  font-weight: 500;
  color: var(--uir-accent);
  background: var(--uir-accent-tint);
}
.uir-pricing-name { font-size: var(--uir-type-h3); font-weight: 600; margin: 0; }
.uir-pricing-price { margin: 0; display: flex; align-items: baseline; gap: .35rem; }
.uir-pricing-amount { font-size: var(--uir-type-h1); font-weight: 700; letter-spacing: var(--uir-track-tight); color: var(--uir-accent); }
.uir-pricing-period { font-size: var(--uir-type-small); color: var(--uir-muted); }
.uir-pricing-features { list-style: none; margin: var(--uir-space-sm) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--uir-space-sm); }
.uir-pricing-feature { display: flex; gap: .5rem; align-items: flex-start; font-size: var(--uir-type-small); }
.uir-pricing-check { color: var(--uir-ok-fg); flex: 0 0 auto; font-weight: 700; }
.uir-pricing-cta { margin-block-start: auto; width: 100%; }

/* ---------------------------------------------------------------
 * 2.5 — small print inside the column.
 * --------------------------------------------------------------- */
.uir-region-footer p { font-size: var(--uir-type-small); color: var(--uir-muted); }

/* ---------------------------------------------------------------
 * 2.7 — the stepper rail: numbered, current filled.
 * --------------------------------------------------------------- */
.uir-stepper-progress {
  display: flex;
  gap: var(--uir-space-lg);
  list-style: none;
  margin: 0 0 var(--uir-space-lg);
  padding: 0;
  flex-wrap: wrap;
}
.uir-stepper-step { display: flex; align-items: center; gap: .5rem; color: var(--uir-muted); font-size: var(--uir-type-small); }
.uir-stepper-ordinal {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--uir-radius-full);
  border: 1px solid var(--uir-hairline);
  font-size: var(--uir-type-label);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.uir-stepper-step.is-active { color: var(--uir-fg); font-weight: 600; }
.uir-stepper-step.is-active .uir-stepper-ordinal {
  background: var(--uir-primary);
  border-color: var(--uir-primary);
  color: var(--uir-on-primary);
}
.uir-stepper-step.is-done .uir-stepper-ordinal {
  background: var(--uir-accent-tint);
  border-color: var(--uir-accent);
  color: var(--uir-accent);
}
.uir-stepper-nav { display: flex; gap: var(--uir-space-sm); margin-block-start: var(--uir-space-lg); }
/* Back is SECONDARY and Next/submit is primary — the one place a
 * stepper says which way is forward. */
.uir-stepper-back {
  background: transparent;
  color: var(--uir-fg);
  border: 1px solid var(--uir-hairline);
  border-radius: var(--uir-radius-sm);
  padding: .6rem 1.1rem;
  font: inherit;
  cursor: pointer;
}
.uir-stepper-back:disabled { opacity: .45; cursor: default; }
.uir-stepper-nav .uir-action-primary { margin-inline-start: auto; }

/* ---------------------------------------------------------------
 * 2.8 — the footer: three columns, tappable contact.
 * --------------------------------------------------------------- */
.uir-storefoot {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--uir-space-xl);
  padding-block: var(--uir-space-xl);
  margin-block-start: var(--uir-space-xl);
  border-top: 1px solid var(--uir-hairline);
  background: var(--uir-card);
  font-size: var(--uir-type-small);
  color: var(--uir-muted);
}
[data-uir-class="compact"] .uir-storefoot { grid-template-columns: 1fr; gap: var(--uir-space-lg); }
.uir-storefoot-col { display: flex; flex-direction: column; gap: .35rem; min-width: 0; }
.uir-storefoot-name { font-size: var(--uir-type-h3); font-weight: 600; color: var(--uir-fg); }
.uir-storefoot-head { font-size: var(--uir-type-label); font-weight: 500; color: var(--uir-fg); }
.uir-storefoot-link { color: var(--uir-accent); text-decoration: none; }
.uir-storefoot-link:hover { text-decoration: underline; }

/* ---------------------------------------------------------------
 * 4.1 – 4.4 — lists and tables. Flat, hairline, 48px rows, no zebra.
 * --------------------------------------------------------------- */
table.uir-list,
table.uir-bounded-grid { width: 100%; border-collapse: collapse; }
.uir-th {
  text-align: start;
  padding: var(--uir-space-sm) var(--uir-space-md);
  border-bottom: 1px solid var(--uir-hairline);
  white-space: nowrap;
}
.uir-td {
  padding: 0 var(--uir-space-md);
  height: 48px;
  border-bottom: 1px solid var(--uir-hairline);
  vertical-align: middle;
}
/* The first column is the record's name — 600, so a scan down the
 * table reads as a list of things rather than a grid of values. */
table.uir-list tr > .uir-td:first-child { font-weight: 600; }
table.uir-list tr:hover > .uir-td { background: var(--uir-tint); }
table.uir-list tr[role="row"] { cursor: pointer; }
table.uir-list tr.uir-row-selected > .uir-td { background: var(--uir-accent-tint); }
/* 4.2: money right, and its header with it. */
.uir-td.uir-format-money,
.uir-th.uir-format-money { text-align: end; font-variant-numeric: tabular-nums; }
/* 4.2: a lifecycle value keeps its own line and its resolved tone —
 * the tone class comes from the cell's `component_role`, which the
 * renderer decided. */
.uir-td.uir-format-enum_lifecycle { white-space: nowrap; }
/* 4.4: the chevron column — as narrow as the glyph. */
.uir-th-chevron,
.uir-td-chevron { width: 1.5rem; text-align: end; color: var(--uir-muted); }
/* 4.1: NO card per row at 1280 — the compact branch renders cards and
 * nothing else may. 4.3: at 390 a row IS a card. */
[data-uir-class="expanded"] .uir-list-cards,
[data-uir-class="medium"] .uir-list-cards { display: none; }
[data-uir-class="compact"] .uir-list-cards { display: flex; flex-direction: column; gap: var(--uir-space-md); }
[data-uir-class="compact"] .uir-list-cards .uir-mini-card { position: relative; padding-inline-end: 6rem; }
/* The status value sits in the card's corner as a pill, and its label
 * goes — "Status" over a pill that says "Placed" is the same word
 * twice. */
[data-uir-class="compact"] .uir-list-cards .uir-kv:has([class*="uir-status-"]) {
  position: absolute;
  inset-block-start: var(--uir-space-md);
  inset-inline-end: var(--uir-space-md);
  margin: 0;
}
[data-uir-class="compact"] .uir-list-cards .uir-kv:has([class*="uir-status-"]) > .uir-kv-label { display: none; }

/* ---------------------------------------------------------------
 * 2.6 / 3.4 — the record card: label left, value right, titled
 * sections, figures in the side column.
 * --------------------------------------------------------------- */
.uir-detail {
  display: grid;
  grid-template-columns: minmax(8rem, 12rem) 1fr;
  gap: var(--uir-space-sm) var(--uir-space-lg);
  margin: 0;
  align-items: baseline;
}
.uir-detail > dt { margin: 0; }
.uir-detail > dd { margin: 0; font-size: 16px; color: var(--uir-fg); }
.uir-detail-section { margin-block-start: var(--uir-space-lg); }
.uir-detail-section + .uir-detail-section { border-top: 1px solid var(--uir-hairline); padding-block-start: var(--uir-space-lg); }
[data-uir-class="compact"] .uir-detail { grid-template-columns: 1fr; gap: .15rem; }
[data-uir-class="compact"] .uir-detail > dd { margin-block-end: var(--uir-space-md); }
/* The figures beside a record: quiet tiles, not cards. */
.uir-stat {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  padding: var(--uir-space-md);
  border: 1px solid var(--uir-hairline);
  border-radius: var(--uir-radius-md);
  background: var(--uir-card);
}
.uir-stat-value { font-size: var(--uir-type-h2); font-weight: 600; letter-spacing: var(--uir-track-tight); }
/* 3.4: the actions row under the title band — the FIRST one primary.
 * The `inspect` recipe puts the action row first; this makes the
 * leading button the loud one and the rest quiet, so a record page
 * has one obvious verb and several available ones. */
.uir-grid-row:first-child .uir-action-standalone ~ .uir-action-standalone button {
  background: transparent;
  color: var(--uir-fg);
  border: 1px solid var(--uir-hairline);
}

/* ---------------------------------------------------------------
 * 5.2 – 5.4 — controls.
 * --------------------------------------------------------------- */
.uir-field { display: flex; flex-direction: column; gap: .35rem; }
.uir-required-mark { color: var(--uir-danger); margin-inline-start: .15rem; }
.uir-field input,
.uir-field select,
.uir-field textarea,
.uir-narrow-text,
.uir-narrow-search,
.uir-narrow-sort {
  width: 100%;
  min-height: 40px;
  box-sizing: border-box;
  padding: .5rem .75rem;
  border: 1px solid var(--uir-hairline);
  border-radius: var(--uir-radius-sm);
  background: var(--uir-card);
  color: var(--uir-fg);
  font: inherit;
}
.uir-field textarea { min-height: 6rem; }
.uir-field input:focus-visible,
.uir-field select:focus-visible,
.uir-field textarea:focus-visible {
  outline: 2px solid var(--uir-focus-ring, var(--uir-accent));
  outline-offset: 1px;
  border-color: var(--uir-focus-ring, var(--uir-accent));
}
/* 5.3: the refusal, inside the form and above the fields. */
.uir-refusal {
  border: 1px solid var(--uir-warn-fg);
  border-radius: var(--uir-radius-sm);
  background: var(--uir-warn-bg);
  color: var(--uir-warn-fg);
  padding: var(--uir-space-md);
  margin-block-end: var(--uir-space-md);
}
/* 3.3: labelled chips, and a sort control that says it sorts. */
.uir-narrowing { display: flex; flex-wrap: wrap; gap: var(--uir-space-md); align-items: center; margin-block-end: var(--uir-space-md); }
.uir-chips { display: flex; flex-wrap: wrap; gap: var(--uir-space-xs); }
.uir-chip {
  border: 1px solid var(--uir-hairline);
  border-radius: var(--uir-radius-full);
  background: var(--uir-card);
  color: var(--uir-muted);
  padding: .3rem .75rem;
  font: inherit;
  font-size: var(--uir-type-label);
  cursor: pointer;
}
.uir-chip[aria-pressed="true"] { background: var(--uir-accent-tint); color: var(--uir-accent); border-color: var(--uir-accent); }
.uir-narrow-sortwrap { display: flex; align-items: center; gap: .5rem; }
.uir-narrow-sortwrap .uir-narrow-sort { width: auto; min-width: 10rem; }

/* ---------------------------------------------------------------
 * 1.8 — the empty state: one line, and a way forward.
 * --------------------------------------------------------------- */
.uir-empty-state {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--uir-space-md);
  padding: var(--uir-space-xl) 0;
}
.uir-empty { margin: 0; max-width: 52ch; }

/* ---------------------------------------------------------------
 * Buttons. One primary shape, one secondary, and the pill for a
 * chip — the radius hierarchy, applied.
 * --------------------------------------------------------------- */
.uir-action-primary,
button.uir-action-primary {
  background: var(--uir-primary);
  color: var(--uir-on-primary);
  border: 0;
  border-radius: var(--uir-radius-sm);
  padding: .6rem 1.1rem;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.uir-action-primary:hover { background: var(--uir-primary-hover, var(--uir-primary)); }
.uir-action-primary:disabled { opacity: .5; cursor: default; }
.uir-titlebar-action { white-space: nowrap; }
.uir-cart {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  border: 1px solid var(--uir-hairline);
  border-radius: var(--uir-radius-full);
  background: var(--uir-card);
  color: var(--uir-fg);
  padding: .35rem .8rem;
  font: inherit;
  font-size: var(--uir-type-small);
  cursor: pointer;
}
.uir-cart-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.35rem;
  height: 1.35rem;
  border-radius: var(--uir-radius-full);
  background: var(--uir-accent);
  color: var(--uir-on-primary);
  font-size: var(--uir-type-label);
  font-weight: 600;
}

/* ---------------------------------------------------------------
 * POLISH-1, part two — the lifecycle PILL (1.6, 4.2, 4.3).
 *
 * A status is never plain text where a pill exists. The renderer
 * resolves the tone (`render.rs`'s `lifecycle_role` — the column's
 * authored `presentation:` first, then a small shallow table over the
 * value) and emits it as the cell's `component_role`; this draws it.
 *
 * ONE rule set, four homes: a table cell, a detail value, a card cell
 * and the compact row-card's corner. Before this the tag reached the
 * DOM as a hook class with no rule behind it, so a status rendered as
 * grey text everywhere — which is the thing 1.6 names.
 *
 * The tone is carried as a CLASS rather than a data attribute because
 * that is what every other role on this wire already uses
 * (`roleClassOf`), and adding a second mechanism for one family would
 * be the drift this file keeps recording. A design package overrides
 * a tone by restating the four `--uir-tone-*` custom properties, which
 * is the same override surface every other colour has.
 * --------------------------------------------------------------- */
:root {
  --uir-tone-ready-bg: var(--uir-ok-bg);
  --uir-tone-ready-fg: var(--uir-ok-fg);
  --uir-tone-progress-bg: var(--uir-accent-tint);
  --uir-tone-progress-fg: var(--uir-accent);
  --uir-tone-warning-bg: var(--uir-warn-bg);
  --uir-tone-warning-fg: var(--uir-warn-fg);
  --uir-tone-critical-bg: color-mix(in srgb, var(--uir-danger) 12%, transparent);
  --uir-tone-critical-fg: var(--uir-danger);
  --uir-tone-neutral-bg: var(--uir-tint-strong);
  --uir-tone-neutral-fg: var(--uir-muted);
}

/* The pill itself. Applied to the SPAN a card cell wraps its value in
 * and to the `<td>`/`<dd>` a table and a record card use — which is
 * why the shape is `display: inline-flex` with its own padding rather
 * than a block: a `<td>` painted edge to edge would be a coloured row,
 * not a pill. */
.uir-status-ready,
.uir-status-progress,
.uir-status-warning,
.uir-status-critical,
.uir-status-neutral {
  display: inline-flex;
  align-items: center;
  border-radius: var(--uir-radius-full);
  padding: .15rem .6rem;
  font-size: var(--uir-type-label);
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
}
.uir-status-ready { background: var(--uir-tone-ready-bg); color: var(--uir-tone-ready-fg); }
.uir-status-progress { background: var(--uir-tone-progress-bg); color: var(--uir-tone-progress-fg); }
.uir-status-warning { background: var(--uir-tone-warning-bg); color: var(--uir-tone-warning-fg); }
.uir-status-critical { background: var(--uir-tone-critical-bg); color: var(--uir-tone-critical-fg); }
.uir-status-neutral { background: var(--uir-tone-neutral-bg); color: var(--uir-tone-neutral-fg); }

/* A `<td>` cannot be an inline-flex box and still be a table cell, so
 * the cell stays a cell and paints its pill on an inner box. The
 * adapter wraps a toned table value in a `<span>` for exactly this. */
.uir-td.uir-status-ready,
.uir-td.uir-status-progress,
.uir-td.uir-status-warning,
.uir-td.uir-status-critical,
.uir-td.uir-status-neutral {
  display: table-cell;
  background: none;
  color: inherit;
  padding: 0 var(--uir-space-md);
}
.uir-td > .uir-pill {
  display: inline-flex;
  align-items: center;
  border-radius: var(--uir-radius-full);
  padding: .15rem .6rem;
  font-size: var(--uir-type-label);
  font-weight: 500;
  white-space: nowrap;
}

/* The title band's pill reuses the badge tones the renderer already
 * emits for a `badge` node, so a record's status reads the same in
 * the band and in the card. */
.uir-badge-tone-success { background: var(--uir-tone-ready-bg); color: var(--uir-tone-ready-fg); }
.uir-badge-tone-info { background: var(--uir-tone-progress-bg); color: var(--uir-tone-progress-fg); }
.uir-badge-tone-warning { background: var(--uir-tone-warning-bg); color: var(--uir-tone-warning-fg); }
.uir-badge-tone-danger { background: var(--uir-tone-critical-bg); color: var(--uir-tone-critical-fg); }
.uir-badge-tone-neutral { background: var(--uir-tone-neutral-bg); color: var(--uir-tone-neutral-fg); }

/* ---------------------------------------------------------------
 * POLISH-1: the hero's picture on the product host.
 *
 * `object-position` centred at the top: a hero photograph is almost
 * always a scene whose subject is above the middle, and a centred
 * crop at 22rem cuts heads off.
 * --------------------------------------------------------------- */
.uir-hero-image { object-position: center 35%; }

/* ---------------------------------------------------------------
 * POLISH-1e (2026-09-12) — the three things the live 1280 screenshots
 * showed, after the branch was applied to :9400.
 *
 * Same discipline as the POLISH-1 section above: last in the file,
 * equal specificity, cascade order is what makes it win.
 * --------------------------------------------------------------- */

/* The rail's LINKS scroll; the brand above and the chip below do not.
 *
 * The rail was one flex column with the chip pushed down by
 * `margin-block-start: auto`. With eleven nav entries plus a Records
 * group plus a Workspace group the column overflowed its own height
 * and the chip painted ON TOP of the last Workspace links — chip text
 * clipped over a faint name underneath, on the live screenshot.
 * `auto` cannot push what there is no room for.
 *
 * One scrolling box between two fixed ones cannot overlap, however
 * long the middle gets. `overscroll-behavior: contain` keeps a flick
 * at the end of the nav from scrolling the page behind it. */
.uir-rail { overflow: hidden; }
.uir-rail-scroll {
  display: flex;
  flex-direction: column;
  gap: var(--uir-space-lg);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.uir-rail > .uir-brand,
.uir-rail > .uir-userchip-wrap { flex: 0 0 auto; }
.uir-userchip-wrap { margin-block-start: var(--uir-space-md); }
/* The divider that says "this is a different kind of thing", now that
 * the chip is no longer separated by whatever space was left over. */
.uir-rail > .uir-userchip-wrap {
  border-top: 1px solid rgb(255 255 255 / 12%);
  padding-block-start: var(--uir-space-sm);
}

/* POLISH-1j: the VISIBLE sign-out, beside the chip in the rail footer.
 *
 * Full width and quiet — it sits under the person's own name, which is
 * the one place on a console where a destructive-looking control reads
 * as "about me" rather than "about this page". Rail colours, because
 * the rail is dark and the shared `.uir-access-btn` is written for the
 * page. */
.uir-rail-signout { margin: 0; margin-block-start: var(--uir-space-xs); }
.uir-rail-signout .uir-access-btn {
  width: 100%;
  text-align: start;
  border-color: rgb(255 255 255 / 18%);
  color: rgb(255 255 255 / 72%);
}
.uir-rail-signout .uir-access-btn:hover {
  color: #fff;
  background: rgb(255 255 255 / 10%);
}
/* And at COMPACT, in the title bar — where `topBar` draws it because
 * the rail has become a bottom icon bar with no footer. Pushed right,
 * exactly as `.uir-topbar > .uir-access` is. */
.uir-topbar > .uir-topbar-signout { margin: 0; margin-inline-start: auto; flex: 0 0 auto; }

/* The viewer's own controls — locale and appearance — wherever the
 * frame put them.
 *
 * They were a right-aligned row in the BODY under the title band, on
 * both frames, although the Design Package has always said
 * `header.utilities: [locale_picker, appearance_picker]`. Now: the
 * storefront header's utility cluster at width, the Menu drawer at
 * 390, and the dashboard's user-chip menu. Small enough in all three
 * that they do not compete with the page. */
.uir-chrome {
  display: flex;
  align-items: center;
  gap: var(--uir-space-xs);
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
}
.uir-chrome-group { display: flex; align-items: center; gap: var(--uir-space-xs); margin: 0; }
.uir-chrome-select {
  min-height: 32px;
  padding: .2rem 1.4rem .2rem .5rem;
  border: 1px solid var(--uir-hairline);
  border-radius: var(--uir-radius-sm);
  background: var(--uir-card);
  color: var(--uir-fg);
  font: inherit;
  font-size: var(--uir-type-label);
}
/* ONE square toggle carrying a glyph, not three words. Its accessible
 * name says the state AND the destination — a glyph alone says
 * neither, which is why `aria-label` is not optional here. */
.uir-chrome-theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--uir-hairline);
  border-radius: var(--uir-radius-sm);
  background: var(--uir-card);
  color: var(--uir-muted);
  font: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.uir-chrome-theme-toggle:hover { color: var(--uir-fg); background: var(--uir-tint); }

/* Inside the rail's user menu the two controls stack and take the
 * menu's own width — a menu is a column, and a select squeezed beside
 * a toggle in a 288px rail is narrower than its own longest option. */
.uir-usermenu .uir-chrome { flex-direction: column; align-items: stretch; gap: var(--uir-space-xs); }
.uir-usermenu .uir-chrome-group { width: 100%; }
.uir-usermenu .uir-chrome-select { width: 100%; }
.uir-usermenu .uir-chrome-theme-toggle { width: 100%; }

/* In the phone drawer they sit under the nav links, separated from
 * them, because they are not places to go. */
[data-uir-class="compact"] .uir-navdrawer-panel .uir-chrome {
  margin-block-start: var(--uir-space-sm);
  padding-block-start: var(--uir-space-sm);
  border-top: 1px solid var(--uir-hairline);
}

/* ---------------------------------------------------------------
 * POLISH-1f (2026-09-12) — the six findings from the 42-page phase-2
 * measurement at 1280 and 390 (`/tmp/cr/p2shots.log`).
 *
 * Same discipline as the two sections above: last in the file, equal
 * or higher specificity than the rule each replaces, cascade order
 * does the rest.
 * --------------------------------------------------------------- */

/* 3.4 / 1.1 — the grid gets the whole content column.
 *
 * `.uir-region-body` is itself a CSS grid, and `pattern: split` /
 * `master_detail` give it two columns — right for the pre-LAYOUT-1
 * main+side pair, wrong once a twelve-column grid is the only child.
 * The grid took column one and column two stood empty: a ~280px
 * detail card in a ~990px body, a third of the frame blank, and the
 * gutter reading 57px on record pages and 117px on the job page
 * instead of 48.
 *
 * `.uir-body-gridded` is set by the adapter exactly when it draws a
 * grid, so the pair keeps its columns everywhere else — at `compact`,
 * and on any render the renderer gave no grid. */
.uir-region-body.uir-body-gridded,
[data-uir-class="expanded"] .uir-pattern-split .uir-region-body.uir-body-gridded,
[data-uir-class="expanded"] .uir-pattern-master_detail .uir-region-body.uir-body-gridded {
  display: block;
}
.uir-body-gridded > .uir-grid { width: 100%; }

/* 1.9 at 390 — a value must wrap rather than widen the page.
 *
 * Measured scrollWidth against a 390 viewport: 481 (customer), 486
 * (invoice), 485 (subscription), 438 (outage), 426 (order), 400
 * (offer). Every one is a record page, and the cause is the same in
 * each: an unbreakable token — an order number, a hashed id, a long
 * address — in a `dd` or a table cell. `anywhere` rather than
 * `break-word` because the tokens have no break opportunity at all,
 * which is exactly the case `break-word` still refuses. */
.uir-detail > dd,
.uir-detail > dt,
.uir-kv,
.uir-kv > span,
.uir-td,
.uir-card-title,
.uir-stat-value {
  min-width: 0;
  overflow-wrap: anywhere;
}
/* A record page at 390 is one column: the detail card and the tiles
 * stack, and neither may hold a minimum wider than the screen. */
[data-uir-class="compact"] .uir-region-main,
[data-uir-class="compact"] .uir-region-side,
[data-uir-class="compact"] .uir-card,
[data-uir-class="compact"] .uir-stat { min-width: 0; max-width: 100%; }
[data-uir-class="compact"] .uir-region-body { grid-template-columns: minmax(0, 1fr); }

/* 4.1 / 4.2 — ONE rule for every table header and every money cell.
 *
 * The pre-POLISH rule is `.uir-list th` (0,1,1), which outranks the
 * class this section introduced (0,1,0) — so table headers kept their
 * uppercase 600 wherever that rule reached, and the measurement found
 * two lists rendering 13/uppercase/600 against everything else at
 * 13/none/500. Matching BOTH the element and the class settles it at
 * one specificity that nothing above can beat.
 *
 * And the money class is `uir-format-money_currency`, not
 * `uir-format-money`: `FormatKind`'s tag is `money_currency` and the
 * earlier selector matched nothing at all. The substring form catches
 * both spellings so a future money format cannot fall out of it. */
.uir-list th.uir-th,
.uir-bounded_grid th.uir-th,
th.uir-th {
  font-size: var(--uir-type-label);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--uir-muted);
  text-align: start;
  padding: var(--uir-space-sm) var(--uir-space-md);
  border-bottom: 1px solid var(--uir-hairline);
}
.uir-list td.uir-td,
.uir-bounded_grid td.uir-td,
td.uir-td {
  padding: 0 var(--uir-space-md);
  height: 48px;
  border-bottom: 1px solid var(--uir-hairline);
  vertical-align: middle;
}
/* The first column is the record's name. */
.uir-list tr > td.uir-td:first-child { font-weight: 600; }
/* Money right, header with it. `[class*=]` because the tag is
 * `money_currency` and a cell carries the hook as one class among
 * several. */
td.uir-td[class*="uir-format-money"],
th.uir-th[class*="uir-format-money"] {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

/* 2.4 — a feature line is its label then its value.
 *
 * The value alone is meaningless for a numeric field: the live cards
 * printed "300" and "0". Muted label, ink value, no colon — the same
 * pairing every other label/value on the page uses. */
.uir-pricing-feature { align-items: baseline; gap: .4rem; flex-wrap: wrap; }
.uir-pricing-feature-label { color: var(--uir-muted); }
.uir-pricing-feature-value { color: var(--uir-fg); font-weight: 500; }

/* 2.5 — small print.
 *
 * `caption` is the third `typography_role`, emitted by the renderer
 * for a landing's TRAILING run of prose (`render::mark_small_print`).
 * It needs no authoring and no `region:`; see that function for why. */
.uir-caption {
  font-size: var(--uir-type-small);
  color: var(--uir-muted);
  max-width: 70ch;
}

/* 3.1 residual — the user chip's name was near-invisible on the navy
 * rail.
 *
 * `--uir-rail-fg` is the rail's own ink and reads correctly on it;
 * the chip's NAME was inheriting the muted tone meant for the role
 * line beneath it, which is a second-line colour on a first-line
 * string. The name takes the rail's full ink and the role keeps the
 * muted one, which is the contrast the pair was designed around. */
.uir-rail .uir-userchip-name { color: var(--uir-rail-fg, var(--uir-fg)); font-weight: 600; }
.uir-rail .uir-userchip-role { color: var(--uir-rail-muted, var(--uir-muted)); }

/* ---------------------------------------------------------------
 * POLISH-1g (2026-09-12) — measured on the LIVE host at 1280
 * (`/backoffice … #/order/ORD-0103DF82`, Playwright against :9400).
 *
 * The two findings reported as "POLISH-1f not effective" were not
 * ineffective: the host was serving POLISH-1e. `uir-adapter.js` and
 * this file are `include_str!`'d into the host binary, so rebuilding
 * the renderer and the wasm does not refresh them — only rebuilding
 * `ubos-studio-web` / `ubos-tenant-app-web` does. Both were verified
 * by injecting the 1f class and rule into the running page and
 * re-measuring; the numbers are in POLISH-1g's own commit message and
 * in the guard below.
 *
 * What IS new here: the rail's Workspace entries and the pricing
 * card's alignment.
 * --------------------------------------------------------------- */

/* A nav item is never an underlined link.
 *
 * The rail's own nav entries are `<button>`, so nothing needed to say
 * this; the Workspace entries are `<a>`, and they inherited the
 * browser's underline. Measured: 12.8px underlined beside 14.4px
 * plain. Said once, on the class, so the next anchor-shaped nav entry
 * cannot reintroduce it. */
.uir-navlink,
.uir-navlink:hover { text-decoration: none; }

/* The pricing card is LEFT aligned, per the design plan's "left
 * aligned" and its rejected-defaults list.
 *
 * `.uir-preset-pricing .uir-mini-card` (0,2,0) centres the preset's
 * generic card and outranked POLISH-1's `.uir-pricing-card` (0,1,0),
 * so the plan card's name and price drew centred. Matching the preset
 * AND the card settles it at the same specificity, later in the file.
 * The button stays full width — it is the card's one action and a
 * left-aligned button in a column of cards reads as three different
 * widths. */
.uir-preset-pricing .uir-pricing-card {
  text-align: start;
  align-items: stretch;
}
.uir-preset-pricing .uir-pricing-card .uir-pricing-tag { align-self: flex-start; }
.uir-preset-pricing .uir-pricing-cta { width: 100%; }
/* The preset's own generic-card rules do not apply to a shaped plan
 * card: it has no `.uir-kv` pairs and no `.uir-card-actions`, and
 * leaving them matched would centre an action row that is not there. */
.uir-preset-pricing .uir-pricing-card .uir-pricing-price { justify-content: flex-start; }

/* ---------------------------------------------------------------
 * POLISH-1g — the table rules, keyed on the STRUCTURAL class.
 *
 * The live cause, measured: a console list's table carries
 * `uir-workspace-queue`, not `uir-list` — a console shell's Design
 * Package assigns `workspace.queue` to its lists. Every rule POLISH-1
 * and POLISH-1f keyed on `.uir-list` therefore missed every console
 * table, which is why the first column stayed 400 there and why the
 * header casing differed on exactly the two tables whose role IS
 * `list` (the pre-POLISH `.uir-list th` reached those and nothing
 * else).
 *
 * A ROLE is what a Design Package themes. "This is a data table" is
 * not a theming decision and must not depend on one. The adapter puts
 * `uir-table` on every data table it draws; these rules key on that,
 * and the role class keeps doing theming.
 * --------------------------------------------------------------- */
table.uir-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: auto;
}
table.uir-table th.uir-th {
  font-size: var(--uir-type-label);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--uir-muted);
  text-align: start;
  padding: var(--uir-space-sm) var(--uir-space-md);
  border-bottom: 1px solid var(--uir-hairline);
  background: var(--uir-card);
}
table.uir-table td.uir-td {
  padding: 0 var(--uir-space-md);
  height: 48px;
  border-bottom: 1px solid var(--uir-hairline);
  vertical-align: middle;
  font-weight: 400;
}
/* The first column is the record's NAME, so a scan down the table
 * reads as a list of things rather than a grid of values. */
table.uir-table tr > td.uir-td:first-child { font-weight: 600; }
table.uir-table tr:hover > td.uir-td { background: var(--uir-tint); }
table.uir-table tr[role="row"] { cursor: pointer; }
table.uir-table tr.uir-row-selected > td.uir-td { background: var(--uir-accent-tint); }
/* Money right, header with it. `[class*=]` because the format tag is
 * `money_currency` and the hook rides as one class among several. */
table.uir-table td.uir-td[class*="uir-format-money"],
table.uir-table th.uir-th[class*="uir-format-money"] {
  text-align: end;
  font-variant-numeric: tabular-nums;
}
table.uir-table td.uir-td-chevron,
table.uir-table th.uir-th-chevron {
  width: 1.5rem;
  text-align: end;
  color: var(--uir-muted);
  font-weight: 400;
}


/* ===================================================================
 * POLISH-1g — the board pass
 * ===================================================================
 *
 * Four findings, each measured on a screenshot rather than reasoned
 * about: the compact dashboard's lost bottom bar, the enum filter
 * that drew two controls, the surface bar (above, where the chooser
 * used to be), and the record page that had no record.
 * --------------------------------------------------------------- */

/* The bottom bar's "More" — the phone's home for the Records group,
 * the Workspace group and the user chip.
 *
 * Drawn as one more bar entry, so the bar is still a row of equal
 * destinations; the sheet opens UPWARD, because the bar is pinned to
 * the bottom edge and a menu that dropped below it would be off
 * screen. */
.uir-railmore { position: relative; flex: 0 0 auto; }
.uir-railmore-btn { flex-direction: column; font-size: .7rem; }
.uir-railsheet {
  position: absolute;
  inset-inline-end: 0;
  bottom: calc(100% + var(--uir-space-sm));
  z-index: 30;
  min-width: 13rem;
  max-height: 60vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--uir-space-sm);
  padding: var(--uir-space-sm);
  border-radius: var(--uir-radius-md);
  background: var(--uir-rail-bg, var(--uir-card));
  box-shadow: var(--uir-shadow-lg);
}
.uir-railsheet[hidden] { display: none; }
/* Inside the sheet the chip is a row like the others: the desktop
 * rail's sticky-bottom divider has nothing to divide here. */
.uir-railsheet > .uir-userchip-wrap { margin-block-start: 0; border-top: 0; }

/* A free-text filter is a labelled box, not a chip strip with one box
 * wedged into it. `.uir-chips` is a wrapping row of pills and gave a
 * lone input the pills' spacing; this is the box's own shape. */
.uir-narrowfield { display: flex; align-items: center; min-width: 0; }
.uir-narrowfield > .uir-narrow-text { min-width: 0; }

/* The scrolling box is the DESKTOP rail's overflow guard (POLISH-1e).
 * In a row-flex bottom bar it must not claim the free space or grow a
 * scroller of its own.
 *
 * Stated here, after the rule it overrides, rather than up in the
 * compact block: the compact block is earlier in this file, and an
 * override that precedes its base is a rule that only works by
 * specificity accident. */
@media (max-width: 640px) {
  .uir-frame-dashboard > .uir-rail > .uir-rail-scroll {
    flex: 0 1 auto;
    min-width: 0;
    overflow: visible;
  }
}


/* ===================================================================
 * POLISH-1h — the interaction proof
 * ===================================================================
 *
 * The viewer's own controls carry WORDS again (POLISH-1e had removed
 * them), and the header cluster is the ONE place that hides them.
 *
 * That inversion is the point. POLISH-1e's argument — no room in a
 * header for a word in front of a control you can read at a glance —
 * holds for the header and nowhere else, and applying it everywhere
 * left a bare list of locale tags and a lone glyph in the phone's
 * Menu drawer and behind the dashboard's user chip, both of which are
 * columns of full-width rows with room to spare. Default to saying
 * what a control is; hide the word where it genuinely will not fit.
 * --------------------------------------------------------------- */
.uir-chrome-label { font-size: var(--uir-type-label); }
.uir-chrome-theme-toggle { display: inline-flex; align-items: center; gap: var(--uir-space-xs); }
.uir-chrome-glyph { line-height: 1; }
/* POLISH-1i: the CHILD combinator, and that is the whole fix.
 *
 * This read `.uir-topnav-utilities .uir-chrome-label` — a descendant
 * selector — and the compact storefront's Menu DRAWER is built INSIDE
 * the utilities cluster (`topNavUtilities` puts `navDrawer()` there),
 * so the drawer's own copy of these controls inherited the header's
 * hiding rule. The words were in the markup and invisible, which is
 * why the interaction proof's `innerText` check still found neither
 * "Language" nor "Theme" after POLISH-1h claimed to have restored
 * them. `innerText` excludes hidden text; the note was right about
 * the markup and wrong about the CSS.
 *
 * `>` restricts it to the chrome bar the cluster holds DIRECTLY —
 * the wide-width header utility — and leaves every nested placement
 * (the drawer, the user-chip menu) saying what its controls are. */
.uir-topnav-utilities > .uir-chrome .uir-chrome-label,
.uir-topnav-utilities > .uir-chrome .uir-chrome-words { display: none; }
/* In the drawer and behind the chip, each control is its own row and
 * the word leads it. */
.uir-navdrawer-panel .uir-chrome-group,
.uir-usermenu .uir-chrome-group { gap: var(--uir-space-sm); }
.uir-navdrawer-panel .uir-chrome-theme-toggle,
.uir-usermenu .uir-chrome-theme-toggle { justify-content: flex-start; }


/* ===================================================================
 * POLISH-1i — two gestures that could not be made
 * ===================================================================
 *
 * The dashboard rail is VIEWPORT-TALL and sticky, so the user chip is
 * where it says it is.
 *
 * `.uir-userchip`'s own comment has said "stays in view while the
 * canvas scrolls, the way a real app's account chip does" since
 * SHELL-1, and it has never done it. `position: sticky` needs a
 * scrolling ancestor, and POLISH-1e gave the rail `overflow: hidden`
 * — which disables sticky rather than scoping it. Meanwhile
 * `.uir-frame-dashboard` is a grid with `align-items: stretch`, so
 * the rail is as tall as the PAGE and `.uir-rail-scroll`'s
 * `flex: 1 1 auto` grows to fill it.
 *
 * Measured live on the back-office console at 1280x900 before this
 * rule: the rail was 3512px tall, the chip sat at y=3431, and
 * reaching it meant scrolling 2588px to the bottom of the page. The
 * menu behind it — the locale select, the theme toggle and Sign out —
 * was reachable only from there. Playwright's own click auto-scrolls
 * and therefore "worked", which is how this survived a wave of
 * measurement.
 *
 * After: rail 900px (exactly the viewport, `border-box` because the
 * rail carries padding), chip at y=868 -> 820, IN the viewport at
 * scrollY 0, and the menu opens above it with the select at y=707.
 * The canvas keeps its own 3512px of scroll; only the rail stops
 * growing with it.
 * --------------------------------------------------------------- */
.uir-frame-dashboard > .uir-rail {
  position: sticky;
  top: 0;
  /* `border-box` is load-bearing, not tidiness: the rail has
   * `padding: space-md space-sm`, so a content-box `100vh` measured
   * 948px against a 900px viewport and left the chip 24px below the
   * fold — the same bug, smaller. */
  box-sizing: border-box;
  height: 100vh;
  align-self: start;
}
/* At compact the rail is the bottom bar and must NOT be a
 * viewport-tall column: the compact block sets `position: sticky;
 * bottom: 0` on it, and a 100vh height there would cover the page. */
@media (max-width: 640px) {
  .uir-frame-dashboard > .uir-rail {
    position: sticky;
    top: auto;
    height: auto;
    align-self: auto;
  }
}

/* The record's own tag beside its name — what the record IS, where
 * the status pill says what state it is in. Neutral by construction:
 * a category is not a state, and giving it a lifecycle tone would
 * spend the one signal the pill needs. */
.uir-recordtag {
  display: inline-block;
  padding: .1rem .5rem;
  border-radius: 999px;
  background: var(--uir-tint-strong);
  color: var(--uir-muted);
  font-size: var(--uir-type-label);
  font-weight: 500;
  white-space: nowrap;
}

/* POLISH-1j: a status pill inside a DETAIL row is inline-sized.
 *
 * `board/shots/20260912-194123/ours_rec_case_1280.png`: the case
 * record's Priority "low" drew as a grey bar running from the label to
 * the card's edge. The `.uir-kv .uir-status-*` rules say
 * `display: inline-block`, which is inline-sized on its own — but a
 * detail row's value cell is a grid/flex item, and a block-level
 * formatting context stretches its child to the cell's full width.
 * The table-cell pills were never affected because a `td` sizes to
 * its content.
 *
 * `inline-flex` + `width: auto` + `align-self: start` says the pill
 * is as wide as its word wherever the cell puts it. A pill is a
 * BADGE: its size is a reading of its content, and a full-width one
 * reads as a filled progress bar. */
.uir-kv .uir-status-ready,
.uir-kv .uir-status-progress,
.uir-kv .uir-status-warning,
.uir-kv .uir-status-critical,
.uir-kv .uir-status-neutral,
.uir-detail dd .uir-status-ready,
.uir-detail dd .uir-status-progress,
.uir-detail dd .uir-status-warning,
.uir-detail dd .uir-status-critical,
.uir-detail dd .uir-status-neutral {
  display: inline-flex;
  width: auto;
  align-self: start;
  justify-self: start;
}

/* ===================================================================
 * POLISH-1l — the title bar stopped fitting on a phone
 * ===================================================================
 *
 * REGRESSION from POLISH-1i. The dashboard title bar is ONE
 * non-wrapping flex row — Back, heading, tag, status pill, then the
 * utilities cluster with the page's primary action and its search.
 * That was survivable while the heading was a SCREEN title ("Order",
 * "Case"); 1i made it the RECORD's own name, and a record's name is
 * as long as the record says it is.
 *
 * Measured at 390 on the live host before this rule:
 * `#/order/ORD-0103DF82` scrollWidth 431, `#/case/CASE-08ED66C8` 489,
 * with `BUTTON.uir-titlebar-action` the far edge in both. The
 * children's own right edges tell the story on the case page —
 * back 69, heading 214, tag 316, pill 387, utilities 403 — everything
 * fits until the cluster, and the cluster cannot fit because nothing
 * before it will give.
 *
 * Three properties, and each is doing separate work:
 *
 * - `flex-wrap: wrap` on the bar, so the cluster DROPS to a second
 *   row instead of pushing the page wide. This is the one that
 *   actually fixes the overflow; the other two decide how it looks
 *   when it does.
 * - `min-width: 0` on the heading and the cluster. A flex item's
 *   automatic minimum is its MIN-CONTENT width, so an unbreakable
 *   token like `CASE-08ED66C8` sets a floor the row cannot go under —
 *   this is the same automatic-minimum rule SHELL-1c hit on the
 *   surface chooser, one element along.
 * - `overflow-wrap: anywhere` on the heading, because a record's name
 *   is frequently ONE token with no space to break at, and a heading
 *   that cannot break is a heading that cannot shrink however small
 *   its minimum is allowed to be.
 *
 * The search box's own `min-width: 14rem` is released here too: 224px
 * is a sensible floor on a console and most of a phone.
 *
 * At 1280 nothing changes — the row has never wrapped there and this
 * whole block is inside the compact query.
 *
 * The storefront band already wraps (`.uir-titleband`, SHELL-1c) and
 * measured 390 throughout; its heading gets the same two shrink
 * properties so a longer record name cannot reach the state this bar
 * reached.
 * --------------------------------------------------------------- */
@media (max-width: 640px) {
  .uir-topbar { flex-wrap: wrap; }
  .uir-topbar-title,
  .uir-screen-title {
    min-width: 0;
    flex: 1 1 auto;
    overflow-wrap: anywhere;
  }
  .uir-topbar-utilities { min-width: 0; flex-wrap: wrap; }
  .uir-narrowing-bar { min-width: 0; flex-wrap: wrap; }
  .uir-narrowing-bar .uir-narrow-search { min-width: 0; flex: 1 1 auto; }
  /* The tag and the pill keep their own width — they are short by
   * construction — but must be allowed to move to the next line
   * rather than hold the row open. */
  .uir-topbar > .uir-recordtag,
  .uir-topbar > .uir-statuspill { flex: 0 0 auto; }
}

/* ---------------------------------------------------------------
 * POLISH-3: the money field's currency prefix.
 *
 * One control, two boxes: the code sits inside the field's own frame
 * so the pair reads as a single input rather than as a label that
 * happens to be next to one. The input loses its own border and keeps
 * the wrapper's, which is what makes focus land on the whole control.
 * --------------------------------------------------------------- */
.uir-moneyfield {
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  border: 1px solid var(--uir-border);
  border-radius: var(--uir-radius);
  background: var(--uir-card);
  overflow: hidden;
  max-width: 100%;
}
.uir-moneyfield:focus-within {
  outline: 2px solid var(--uir-focus-ring);
  outline-offset: 1px;
}
.uir-money-prefix {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--uir-space-sm);
  background: var(--uir-tint);
  color: var(--uir-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.uir-moneyfield > input {
  border: 0;
  border-radius: 0;
  background: transparent;
  min-width: 0;
}
.uir-moneyfield > input:focus {
  outline: none;
}

/* ---------------------------------------------------------------
 * POLISH-3: the compact bottom bar clipped its last tab's label.
 *
 * Measured at 390: the tabs share the bar, the labels are sized for
 * Latin, and the last one ran past the right edge — so a tab a thumb
 * reaches for was the one with no readable name. The bar already lays
 * its items out in a row; what it lacked was permission for an item to
 * be NARROWER than its content, and an instruction for what to do when
 * it is.
 *
 * `min-width: 0` is the fix and the rest is consequence: a flex item's
 * automatic minimum is its min-content width, which is what made the
 * row overflow instead of sharing. With that lifted, each label
 * ellipsizes inside its own tab rather than the last one falling off
 * the bar. Scoped to the COMPACT rail, so the expanded sidebar — where
 * there is room and a full label is the point — is untouched.
 * --------------------------------------------------------------- */
@media (max-width: 640px) {
  .uir-frame-dashboard > .uir-rail .uir-navlink {
    min-width: 0;
    flex: 1 1 0;
    overflow: hidden;
  }
  .uir-frame-dashboard > .uir-rail .uir-navlink-label {
    display: block;
    max-width: 100%;
    white-space: nowrap;
  }
}

/* POLISH-3: the control a refusal is about.
 *
 * Colour is not the signal — `aria-invalid` is, and it is set beside
 * this — but the eye needs somewhere to land, and a form of twelve
 * boxes with a sentence at the top is a hunt. Uses the danger token,
 * so a tenant who re-themes gets their own red and not this file's. */
.uir-field-refused,
.uir-moneyfield:has(.uir-field-refused) {
  border-color: var(--uir-danger);
  outline: 1px solid var(--uir-danger);
}
