/* ══════════════════════════════════════════════════════════════════════════
   b2b.css — the component layer for every B2B surface: the platform CRM,
   the centre-admin screens, and the offline-mock booking flow.

   WHY THIS FILE EXISTS
   The B2B templates were built from loose utility classes — `text-xs` 170
   times, `font-bold` 151, `text-gray-500` 141 — so nothing was shared and
   nothing propagated. Worse, those greys are literals that ignore the theme,
   which is why contrast collapsed in dark mode. Everything here is a named
   class driven by a token: change the token, every screen follows.

   NAMESPACE
   Tokens are `--ui-*` and classes are `.ui-*` on purpose. `style.css` defines
   `--primary: #e11d48` (a hex) and `dashboard.css` defines
   `--primary: 240 5.9% 10%` (an HSL triplet) — same name, different meaning,
   different format, load-order decides the winner. A new namespace is the only
   way to add a layer without joining that collision.

   THEME
   The default is the device's. `:root` carries the complete light palette and
   the media query redefines the tokens for a dark device. An explicit choice
   wins over the device in both directions, and this file honours BOTH toggle
   mechanisms already in the codebase: `[data-theme]` (set by base.html) and
   `.dark` (set by dashboard_base.html). No colour is ever declared only inside
   a media or attribute block — that is what makes a page render one theme's
   text on the other theme's ground.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* Surfaces and ink — a cool slate bias, continuing the #334155 / #64748b
     already used across the product rather than introducing a new grey. */
  --ui-bg:        #f8fafc;   /* style.css --bg-base      */
  --ui-surface:   #ffffff;   /* style.css --bg-surface   */
  --ui-surface-2: #f1f5f9;
  --ui-ink:       #0f172a;   /* style.css --text-heading */
  --ui-ink-2:     #334155;   /* style.css --text-main    */
  --ui-ink-3:     #64748b;   /* style.css --text-muted   */
  --ui-rule:      #e2e8f0;   /* style.css --border-color */
  --ui-rule-2:    #cbd5e1;

  /* Brand. Crimson is loud, so it marks exactly one action per screen —
     the primary button, the active nav item, the focus ring. Nothing else. */
  --ui-brand:      #e11d48;
  --ui-brand-ink:  #ffffff;
  --ui-brand-hi:   #be123c;
  --ui-brand-soft: #fdeaef;

  /* Semantic. Muted on purpose: these appear dozens of times per table, and
     saturated status colours turn a roster into a fairground. */
  --ui-ok:      #1f7a4d;  --ui-ok-soft:      #e6f2ec;
  --ui-warn:    #9a6407;  --ui-warn-soft:    #fbf1de;
  --ui-danger:  #a8281f;  --ui-danger-soft:  #f9e8e6;
  --ui-info:    #245ea8;  --ui-info-soft:    #e7eefa;
  --ui-neutral: #5b6577;  --ui-neutral-soft: #eceff4;

  --ui-r:      5px;
  --ui-r-sm:   3px;
  --ui-r-pill: 999px;
  --ui-scrim:  rgba(16, 22, 33, .55);
  --ui-shadow: 0 1px 2px rgba(16, 22, 33, .06), 0 1px 1px rgba(16, 22, 33, .04);

  --ui-sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --ui-mono: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", monospace;
}

/* Dark tokens, written once and referenced from three selectors below.
   Keep these three lists identical — a value that drifts between them is a
   theme bug that only shows up for one class of visitor. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not(.light) {
    --ui-bg:        hsl(180, 1%, 10%);        /* the product's existing dark ground */
    --ui-surface:   hsl(180, 3%, 13%);        /* dashboard.css --card               */
    --ui-surface-2: hsl(180, 3%, 17%);
    --ui-ink:       #ffffff;                  /* style.css --text-heading           */
    --ui-ink-2:     #a1a1aa;                  /* style.css --text-main              */
    --ui-ink-3:     #71717a;                  /* style.css --text-muted             */
    --ui-rule:      rgba(255, 255, 255, .10); /* style.css --border-color           */
    --ui-rule-2:    rgba(255, 255, 255, .18);
    --ui-brand:      #f2264f;
    --ui-brand-ink:  #ffffff;
    --ui-brand-hi:   #ff4267;
    --ui-brand-soft: #2a121a;
    --ui-ok:      #5fc08c;  --ui-ok-soft:      #12251c;
    --ui-warn:    #dda43c;  --ui-warn-soft:    #2a2113;
    --ui-danger:  #ef7a6d;  --ui-danger-soft:  #2b1614;
    --ui-info:    #7aa8e6;  --ui-info-soft:    #131e2d;
    --ui-neutral: #97a1b2;  --ui-neutral-soft: #1d222b;
    --ui-scrim:  rgba(0, 0, 0, .68);
    --ui-shadow: 0 1px 2px rgba(0, 0, 0, .4);
  }
}

:root[data-theme="dark"],
:root.dark {
  --ui-bg:        hsl(180, 1%, 10%);        /* the product's existing dark ground */
  --ui-surface:   hsl(180, 3%, 13%);        /* dashboard.css --card               */
  --ui-surface-2: hsl(180, 3%, 17%);
  --ui-ink:       #ffffff;                  /* style.css --text-heading           */
  --ui-ink-2:     #a1a1aa;                  /* style.css --text-main              */
  --ui-ink-3:     #71717a;                  /* style.css --text-muted             */
  --ui-rule:      rgba(255, 255, 255, .10); /* style.css --border-color           */
  --ui-rule-2:    rgba(255, 255, 255, .18);
  --ui-brand:      #f2264f;
  --ui-brand-ink:  #ffffff;
  --ui-brand-hi:   #ff4267;
  --ui-brand-soft: #2a121a;
  --ui-ok:      #5fc08c;  --ui-ok-soft:      #12251c;
  --ui-warn:    #dda43c;  --ui-warn-soft:    #2a2113;
  --ui-danger:  #ef7a6d;  --ui-danger-soft:  #2b1614;
  --ui-info:    #7aa8e6;  --ui-info-soft:    #131e2d;
  --ui-neutral: #97a1b2;  --ui-neutral-soft: #1d222b;
  --ui-scrim:  rgba(0, 0, 0, .68);
  --ui-shadow: 0 1px 2px rgba(0, 0, 0, .4);
}

/* ── Page scope ────────────────────────────────────────────────────────────
   Scoped to .ui-scope rather than applied to body, so adding this stylesheet
   to a page cannot restyle the student chrome around it. */
.ui-scope {
  background: var(--ui-bg);
  color: var(--ui-ink);
  font-family: var(--ui-sans);
  font-size: 14px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;

  /* The page column. `{% block content %}` renders inside
     `.dashboard-content-wrapper`, which sets only `flex: 1` and a background —
     no width limit and no padding at all. Each old template brought its own
     `.container-fluid`; the converted ones brought none, so content sat flush
     against the sidebar and ran off the right edge. These are
     `.container-fluid`'s own values, so converted pages line up with the
     dashboard pages they sit beside instead of inventing a second geometry. */
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;

  /* A wide table must scroll inside its own wrapper, never widen the page. */
  min-width: 0;
  overflow-x: clip;
}

/* A settings page is still a full-width page. An earlier attempt capped it at
   920px, which left about a third of the content area dead on the right. The
   width now comes from .ui-scope like every other page; readable form columns
   come from the grid below instead of from starving the page. */
.ui-scope--form { /* hook only — kept so templates need not change again */ }

/* Settings grid. Two columns at full width rather than one narrow centred
   column: capping the page at 920px left roughly a third of the content area
   dead on the right, and a single column of full-width form rows pushes a
   label and its input so far apart they stop reading as a pair. Two columns
   fill the space AND keep each form about 600px, which is the width a form
   actually wants. Cards are independent, so they may stagger. */
.ui-settings-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  gap: 18px;
  align-items: start;
}
@media (max-width: 900px) { .ui-settings-grid { grid-template-columns: 1fr; } }

/* A figure that needs to be read at a glance inside a panel — seats used,
   evaluations allowed. Not a .ui-metric: those live in a bordered strip. */
.ui-figure { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.ui-figure__v {
  font-size: 30px; font-weight: 680; letter-spacing: -.02em; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.ui-figure__u { font-size: 13px; color: var(--ui-ink-3); }

/* Proportion bar. Shows headroom, which a bare "5 of 50" does not. */
.ui-bar {
  height: 6px; border-radius: var(--ui-r-pill); overflow: hidden;
  background: var(--ui-surface-2); margin: 12px 0 8px;
}
.ui-bar__fill { height: 100%; background: var(--ui-brand); border-radius: inherit; }
.ui-bar__fill--ok { background: var(--ui-ok); }
.ui-bar__fill--warn { background: var(--ui-warn); }
.ui-scope *,
.ui-scope *::before,
.ui-scope *::after { box-sizing: border-box; }

/* Nothing inside may force the column wider than its share of the row. */
.ui-scope .ui-panel,
.ui-scope .ui-metrics,
.ui-scope .ui-table-wrap { min-width: 0; max-width: 100%; }

/* ── Page shell ───────────────────────────────────────────────────────────
   The dashboard shell supplies its own centred column, but the public
   offline-mock pages extend base.html, which supplies none. Without this they
   have to borrow `.container` from style.css — the last non-ui-* class on an
   otherwise converted page, and a dependency on a stylesheet this layer is
   deliberately independent of. */
.ui-page {
  /* Public pages only. `.navbar` in style.css is `position: fixed` and 90px
     tall, so a page that merely starts at the top of <body> renders its first
     heading UNDERNEATH the logo — which is what the offline-mock landing page
     was doing. The column itself comes from .ui-scope; this only buys the
     clearance the fixed bar takes away, plus room to breathe below it. */
  padding-top: calc(90px + 1.5rem);
  padding-bottom: 4rem;
}
.ui-page--narrow { max-width: 820px; }

/* ── Page header ─────────────────────────────────────────────────────────── */
.ui-crumb {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--ui-ink-3); margin-bottom: 10px;
}
.ui-crumb a { color: var(--ui-ink-2); text-decoration: none; }
.ui-crumb a:hover { color: var(--ui-brand); text-decoration: underline; }

.ui-pagehead {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 18px; flex-wrap: wrap; margin-bottom: 20px;
}
.ui-pagehead h1 {
  font-size: 22px; font-weight: 680; letter-spacing: -.02em;
  margin: 0 0 3px; text-wrap: balance;
}
.ui-pagehead p { margin: 0; color: var(--ui-ink-2); font-size: 13.5px; }
.ui-pagehead__actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* ── Buttons ──────────────────────────────────────────────────────────────
   Destructive actions are outlined, never filled. A filled red button sitting
   beside a filled crimson primary is a misclick waiting to happen. */
.ui-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font: inherit; font-weight: 550; font-size: 13.5px; line-height: 1;
  padding: 9px 15px; border-radius: var(--ui-r);
  border: 1px solid transparent; background: transparent; color: var(--ui-ink);
  cursor: pointer; white-space: nowrap; text-decoration: none;
  transition: background .12s, border-color .12s, color .12s;
}
.ui-btn:focus-visible { outline: 2px solid var(--ui-brand); outline-offset: 2px; }

/* Every hover state restates `color`, and that is not redundant.
   style.css carries a global `a:hover { color: var(--primary-dark) }`, and
   most of these buttons are <a> elements. Setting only `background` here left
   that rule to win, so hovering the primary button turned its label dark
   crimson ON a crimson field — the text vanished. A rule of one class plus one
   pseudo-class outranks `a:hover`, so naming the colour is what fixes it. */
.ui-btn--primary   { background: var(--ui-brand); color: var(--ui-brand-ink); }
.ui-btn--primary:hover,
.ui-btn--primary:focus { background: var(--ui-brand-hi); color: var(--ui-brand-ink); }

.ui-btn--secondary { background: var(--ui-surface); border-color: var(--ui-rule-2); color: var(--ui-ink); }
.ui-btn--secondary:hover,
.ui-btn--secondary:focus { background: var(--ui-surface-2); color: var(--ui-ink); }

.ui-btn--ghost     { color: var(--ui-ink-2); }
.ui-btn--ghost:hover,
.ui-btn--ghost:focus { background: var(--ui-surface-2); color: var(--ui-ink); }

.ui-btn--danger    { background: var(--ui-surface); border-color: var(--ui-danger); color: var(--ui-danger); }
.ui-btn--danger:hover,
.ui-btn--danger:focus { background: var(--ui-danger-soft); color: var(--ui-danger); }
.ui-btn--sm    { padding: 6px 10px; font-size: 12.5px; }
.ui-btn--lg    { padding: 12px 22px; font-size: 15px; }
.ui-btn--block { width: 100%; }
.ui-btn[disabled], .ui-btn[aria-disabled="true"] { opacity: .45; pointer-events: none; }
.ui-btn svg { width: 15px; height: 15px; flex: none; }

/* ── Badges ───────────────────────────────────────────────────────────────
   Colour never carries the meaning alone; the word is always present. */
.ui-badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11.5px; font-weight: 600; line-height: 1;
  padding: 4px 8px; border-radius: var(--ui-r-sm); white-space: nowrap;
  background: var(--ui-neutral-soft); color: var(--ui-neutral);
}
.ui-badge--ok     { background: var(--ui-ok-soft);     color: var(--ui-ok); }
.ui-badge--warn   { background: var(--ui-warn-soft);   color: var(--ui-warn); }
.ui-badge--danger { background: var(--ui-danger-soft); color: var(--ui-danger); }
.ui-badge--info   { background: var(--ui-info-soft);   color: var(--ui-info); }
.ui-badge--quiet  { background: transparent; color: var(--ui-ink-3); padding-left: 0; }
.ui-badge--dot::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: none;
}

/* ── Toolbar ─────────────────────────────────────────────────────────────── */
.ui-toolbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 0 0 14px;
}
.ui-toolbar__spacer { flex: 1 1 auto; }
.ui-toolbar__count  { font-size: 12.5px; color: var(--ui-ink-3); }

.ui-search { position: relative; flex: 0 1 300px; }
.ui-search svg {
  position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; color: var(--ui-ink-3); pointer-events: none;
}
.ui-search .ui-input { padding-left: 32px; }

/* ── Forms ────────────────────────────────────────────────────────────────
   Label above, hint below, error replaces the hint. */
.ui-field { display: block; margin-bottom: 18px; }
.ui-field__label {
  display: block; font-size: 12.5px; font-weight: 600;
  color: var(--ui-ink); margin-bottom: 6px;
}
.ui-field__label .ui-optional { color: var(--ui-ink-3); font-weight: 400; }
.ui-field__hint  { font-size: 12px; color: var(--ui-ink-3); margin-top: 6px; }
.ui-field__error { font-size: 12px; color: var(--ui-danger); margin-top: 6px; font-weight: 550; }

.ui-input, .ui-select, .ui-textarea {
  width: 100%; font: inherit; font-size: 13.5px; color: var(--ui-ink);
  background: var(--ui-surface); border: 1px solid var(--ui-rule-2);
  border-radius: var(--ui-r); padding: 9px 11px;
}
.ui-input::placeholder, .ui-textarea::placeholder { color: var(--ui-ink-3); }
.ui-input:focus, .ui-select:focus, .ui-textarea:focus {
  outline: none; border-color: var(--ui-brand);
  box-shadow: 0 0 0 3px var(--ui-brand-soft);
}
.ui-field--invalid .ui-input,
.ui-field--invalid .ui-select,
.ui-field--invalid .ui-textarea { border-color: var(--ui-danger); }
.ui-textarea { min-height: 76px; resize: vertical; }
.ui-select {
  appearance: none; padding-right: 30px;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 14px center, right 9px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.ui-select--auto { width: auto; }

.ui-check {
  display: flex; align-items: flex-start; gap: 9px;
  font-size: 13.5px; margin-bottom: 10px;
}
.ui-check input {
  margin: 2px 0 0; accent-color: var(--ui-brand);
  width: 15px; height: 15px; flex: none;
}

.ui-form-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0 18px;
}
.ui-form-actions {
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
  margin-top: 14px;
}

/* ── Tables ───────────────────────────────────────────────────────────────
   Hairlines, not card borders. Numerals right-aligned and tabular so a column
   of seat counts can be compared by eye. */
.ui-table-wrap { overflow-x: auto; }
.ui-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.ui-table th {
  text-align: left; font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .07em; color: var(--ui-ink-3); white-space: nowrap;
  padding: 0 14px 8px 0; border-bottom: 1px solid var(--ui-rule-2);
}
.ui-table td {
  padding: 11px 14px 11px 0; border-bottom: 1px solid var(--ui-rule);
  vertical-align: middle;
}
.ui-table tbody tr:last-child td { border-bottom: 0; }
.ui-table tbody tr:hover td { background: var(--ui-surface-2); }
.ui-table th:last-child, .ui-table td:last-child { padding-right: 0; text-align: right; }
.ui-table__num    { text-align: right; font-variant-numeric: tabular-nums; }
.ui-table__strong { font-weight: 600; color: var(--ui-ink); }
.ui-table__sub    { display: block; font-size: 12px; color: var(--ui-ink-3); }
.ui-table__muted  { color: var(--ui-ink-3); }
.ui-table__sort   { color: inherit; text-decoration: none; display: inline-flex; gap: 4px; align-items: center; }
.ui-table__sort[aria-sort]::after { content: "\2193"; font-size: 10px; color: var(--ui-brand); }
.ui-table__sort[aria-sort="descending"]::after { content: "\2191"; }

/* ── Panels and records ──────────────────────────────────────────────────── */
.ui-panel {
  background: var(--ui-surface); border: 1px solid var(--ui-rule);
  border-radius: var(--ui-r); box-shadow: var(--ui-shadow);
}
.ui-panel__head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 14px 18px; border-bottom: 1px solid var(--ui-rule);
}
.ui-panel__title {
  font-size: 13px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ui-ink-3); margin: 0;
}
.ui-panel__body { padding: 18px; }
.ui-panel__body--flush { padding: 18px 18px 6px; }

/* A sentence explaining what a panel is for. It belongs on its own line under
   the title, not appended to the title row — the settings page had a title, a
   badge and a full sentence competing in one flex row, which wrapped badly at
   every width. */
.ui-panel__desc {
  flex-basis: 100%; margin: 2px 0 0;
  font-size: 12.5px; line-height: 1.5; color: var(--ui-ink-3);
}

/* Closing note under a section — "need something changed? contact support". */
.ui-note {
  margin: 16px 0 0; padding-top: 14px;
  border-top: 1px solid var(--ui-rule);
  font-size: 12.5px; color: var(--ui-ink-3);
}
.ui-note a { color: var(--ui-ink-2); text-decoration: underline; }
.ui-note a:hover { color: var(--ui-brand); }

.ui-def { display: grid; grid-template-columns: minmax(140px, auto) 1fr; gap: 0; margin: 0; }
.ui-def dt {
  padding: 10px 18px 10px 0; margin: 0; color: var(--ui-ink-3);
  font-size: 12.5px; border-bottom: 1px solid var(--ui-rule);
}
.ui-def dd {
  padding: 10px 0; margin: 0; border-bottom: 1px solid var(--ui-rule);
  font-weight: 550;
}
.ui-def dt:last-of-type, .ui-def dd:last-of-type { border-bottom: 0; }

/* ── Metric strip ─────────────────────────────────────────────────────────
   Replaces stat-card grids. Figures exist to be compared, so they share one
   baseline inside one container instead of floating in separate boxes. */
.ui-metrics {
  display: flex; flex-wrap: wrap; gap: 0;
  border: 1px solid var(--ui-rule); border-radius: var(--ui-r);
  background: var(--ui-surface); overflow: hidden;
}
.ui-metric { flex: 1 1 130px; padding: 14px 18px; border-right: 1px solid var(--ui-rule); }
.ui-metric:last-child { border-right: 0; }
.ui-metric__v {
  font-size: 24px; font-weight: 680; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; line-height: 1.1;
}
.ui-metric__k { font-size: 12px; color: var(--ui-ink-3); margin-top: 2px; }
.ui-metric--accent .ui-metric__v { color: var(--ui-brand); }

/* ── Notices ─────────────────────────────────────────────────────────────
   Small and inline. Product UI says what is true; it does not explain the
   engineering behind it. */
.ui-notice {
  display: flex; gap: 10px; padding: 11px 14px; border-radius: var(--ui-r);
  font-size: 13px; border: 1px solid; margin-bottom: 12px;
  background: var(--ui-info-soft); border-color: var(--ui-info); color: var(--ui-ink);
}
.ui-notice svg { width: 16px; height: 16px; flex: none; margin-top: 1px; color: var(--ui-info); }
.ui-notice b { font-weight: 650; }
.ui-notice--ok       { background: var(--ui-ok-soft);     border-color: var(--ui-ok); }
.ui-notice--ok svg   { color: var(--ui-ok); }
.ui-notice--warn     { background: var(--ui-warn-soft);   border-color: var(--ui-warn); }
.ui-notice--warn svg { color: var(--ui-warn); }
.ui-notice--danger   { background: var(--ui-danger-soft); border-color: var(--ui-danger); }
.ui-notice--danger svg { color: var(--ui-danger); }

/* ── Stepper ─────────────────────────────────────────────────────────────── */
.ui-steps { display: flex; align-items: center; flex-wrap: wrap; margin-bottom: 26px; }
.ui-step  { display: flex; align-items: center; gap: 9px; font-size: 13px; color: var(--ui-ink-3); }
.ui-step__n {
  width: 24px; height: 24px; border-radius: 50%; flex: none;
  display: grid; place-items: center; font-size: 12px; font-weight: 650;
  border: 1.5px solid var(--ui-rule-2); background: var(--ui-surface);
}
.ui-step--done            { color: var(--ui-ink-2); }
.ui-step--done .ui-step__n { background: var(--ui-ok); border-color: var(--ui-ok); color: #fff; }
.ui-step--now             { color: var(--ui-ink); font-weight: 650; }
.ui-step--now .ui-step__n { background: var(--ui-brand); border-color: var(--ui-brand); color: var(--ui-brand-ink); }
.ui-steps__bar {
  flex: 1 1 24px; height: 1.5px; background: var(--ui-rule-2);
  margin: 0 12px; min-width: 18px;
}

/* ── Modal ────────────────────────────────────────────────────────────────
   Seven confirmation dialogs were each hand-rolled with an inline
   `background: rgba(0,0,0,.55)` — and had already drifted to .6 in three of
   them, which is the exact failure mode this layer exists to prevent. The
   scrim is a token so a single edit moves every dialog in the product.

   Markup:
     <div class="ui-modal" hidden>            <- the scrim
       <div class="ui-modal__box">…</div>     <- the dialog
     </div>
   Toggle the `hidden` attribute, not a display utility, so the element stays
   inert for assistive tech while closed. */
.ui-modal {
  position: fixed; inset: 0; z-index: 50;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  background: var(--ui-scrim);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.ui-modal[hidden] { display: none; }
.ui-modal__box {
  width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto;
  background: var(--ui-surface); border: 1px solid var(--ui-rule);
  border-radius: var(--ui-r); box-shadow: 0 12px 32px rgba(0, 0, 0, .22);
}
.ui-modal__box--wide { max-width: 720px; }

/* ── Empty states ────────────────────────────────────────────────────────── */
.ui-empty     { text-align: center; padding: 44px 20px; color: var(--ui-ink-2); }
.ui-empty__t  { font-weight: 650; color: var(--ui-ink); margin-bottom: 4px; }
.ui-empty__d  { font-size: 13px; max-width: 44ch; margin: 0 auto 16px; }

/* ── Pager ───────────────────────────────────────────────────────────────── */
.ui-pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; padding-top: 14px; margin-top: 2px;
  border-top: 1px solid var(--ui-rule); font-size: 13px; color: var(--ui-ink-3);
}
.ui-pager__links { display: flex; gap: 8px; align-items: center; }

/* ── Layout helpers ──────────────────────────────────────────────────────── */
.ui-stack   { display: flex; flex-direction: column; gap: 18px; }
.ui-row     { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.ui-split   { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.ui-split--aside { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
@media (max-width: 820px) {
  .ui-split, .ui-split--aside { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .ui-scope * { transition: none !important; animation: none !important; }
}
