@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,800&display=swap');

/*
 * FRAUNCES IS IMPORTED HERE, NOT ADDED TO THIRTEEN <head>s.
 *
 * The serif headings below are worth nothing if the face is not loaded: a
 * missing family falls back silently to Plus Jakarta and the page looks
 * merely fine, which is the hardest kind of bug to notice. Only the profile
 * carried Fraunces in its font link; every other v2 page would have shown the
 * fallback.
 *
 * One @import here reaches every page that loads ui.css, including the next
 * one somebody writes. Pages that also carry their own <link> (the profile,
 * the cabinet) are unaffected — the browser dedupes the request.
 *
 * @import has to come before any rule in the file, which is why it sits above
 * even the header comment.
 */
/* ══════════════════════════════════════════════════════════════════════
   PIBERA UI — Design System
   ----------------------------------------------------------------------
   Hand-written, build-free. Load AFTER /public/style.css:

       <link rel="stylesheet" href="/public/style.css">
       <link rel="stylesheet" href="/public/ui.css">

   Every class is prefixed `ui-` so it never collides with Tailwind
   utilities or the legacy classes in style.css.

   Rules of the road:
     1. Never hard-code a colour in a page. Use a token.
     2. Never invent a radius or font-size. Use the scale.
     3. Markup declares INTENT (surface, muted text, tone). The theme decides
        the pixels. No `!important` colour overrides. Ever.

   THEMING CONTRACT
   ----------------
   One switch for the whole product: `data-ui-theme` on <html>.

       <html data-ui-theme="dark">   → dark tokens
       <html data-ui-theme="light">  → light tokens (also the default)

   Every v2 page must set it BEFORE first paint with a blocking head script,
   otherwise the page flashes the wrong theme:

       <script>
         (function () {
           var d = 'light';                    // this page's default
           var m = document.cookie.match(/(?:^|;\s*)pibera_theme_preference=([^;]*)/);
           var t = (m && decodeURIComponent(m[1]))
                || localStorage.getItem('theme-preference') || d;
           document.documentElement.dataset.uiTheme = t === 'dark' ? 'dark' : 'light';
         })();
       </script>

   Portals default to 'light'; the arena and games pages default to 'dark'.
   `PiberaTheme.apply()` keeps the attribute in sync when the user toggles.

   Body must also carry `data-ui="v2"` — that's what tells
   portal-components.js to skip the legacy dark-mode override stylesheet.
   ══════════════════════════════════════════════════════════════════════ */


/* ─── 1. TOKENS ─────────────────────────────────────────────────────── */

/* Mode-independent: scales, motion, spacing. Never themed. */
/* ─── Box model ─────────────────────────────────────────────────────── */
/*
 * The design system owns its own box model.
 *
 * It relied on Tailwind's preflight for this, which happens to be loaded on
 * every current page — so `.ui-input { width: 100% }` plus 28px of padding and
 * a 2px border laid out correctly by borrowing a rule from a stylesheet it
 * does not control. Anywhere Tailwind is absent (a preview, an email template,
 * a page that opts out) every padded full-width control overflows its parent
 * by exactly its padding.
 *
 * Identical to what preflight already sets, so this changes nothing where
 * Tailwind is present and fixes it where it is not.
 */
*, *::before, *::after { box-sizing: border-box; }

:root {
    /* Radius scale — five steps, no exceptions */
    --ui-r-sm: 8px;
    --ui-r-md: 12px;
    --ui-r-lg: 16px;
    --ui-r-xl: 22px;
    --ui-r-2xl: 28px;
    --ui-r-full: 999px;

    /* Spacing rhythm (4px base) */
    --ui-s-1: 4px;
    --ui-s-2: 8px;
    --ui-s-3: 12px;
    --ui-s-4: 16px;
    --ui-s-5: 20px;
    --ui-s-6: 24px;
    --ui-s-8: 32px;
    --ui-s-10: 40px;
    --ui-s-12: 48px;
    --ui-s-16: 64px;

    /* Motion */
    --ui-ease: cubic-bezier(0.4, 0, 0.2, 1);
    --ui-fast: 140ms;
    --ui-base: 220ms;

    /* Fixed hues — these mean the same thing in both modes */
    --ui-on-accent: #ffffff;
    --ui-success: #10b981;
    --ui-warning: #f59e0b;
    --ui-danger: #ef4444;
}

/* ── Private family hub ───────────────────────────────────────────── */
.family-hero {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: var(--ui-s-6);
  padding: clamp(24px, 5vw, 48px) 0 var(--ui-s-6);
}
.family-hero-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(72px, 1fr));
  gap: 1px;
  overflow: hidden;
  min-width: min(100%, 380px);
  border: 1px solid var(--ui-border);
  border-radius: var(--ui-r-lg);
  background: var(--ui-border);
}
.family-hero-stats span {
  display: grid;
  gap: 2px;
  padding: 14px 12px;
  text-align: center;
  background: var(--ui-surface-1);
}
.family-hero-stats strong { color: var(--ui-text); font-size: 1.1rem; }
.family-hero-stats small { color: var(--ui-text-muted); font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
.family-tabs {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  margin-bottom: var(--ui-s-6);
  padding: 5px;
  border: 1px solid var(--ui-border);
  border-radius: var(--ui-r-lg);
  background: var(--ui-surface-1);
  scrollbar-width: thin;
}
.family-tab {
  flex: 0 0 auto;
  min-height: 42px;
  padding: 9px 14px;
  border: 0;
  border-radius: var(--ui-r-md);
  color: var(--ui-text-muted);
  background: transparent;
  font: inherit;
  font-size: .82rem;
  font-weight: 800;
  cursor: pointer;
}
.family-tab:hover { color: var(--ui-text); background: var(--ui-surface-3); }
.family-tab.is-active { color: var(--ui-accent-text); background: var(--ui-accent-soft); }
.family-pane {
  min-width: 0;
  min-height: 320px;
}
.family-pane .ui-card,
.family-pane .ui-ink-panel { min-width: 0; }
.family-pane .ui-card__head,
.family-pane .ui-section-head {
  align-items: flex-start;
}
.family-pane .ui-card__head > div,
.family-pane .ui-section-head > div,
.family-pane .ui-list-row__body { min-width: 0; }
.family-pane :where(.ui-card__title, .ui-card__sub, .ui-section-head__title, .ui-section-head__sub, .ui-list-row__name, .ui-list-row__meta, .ui-body, .ui-text-sm, .ui-text-xs) {
  overflow-wrap: anywhere;
}
.family-pane .ui-list-row__name {
  line-height: 1.4;
  white-space: normal;
}
.family-pane .ui-list-row__meta {
  display: block;
  line-height: 1.5;
}
.family-pane .ui-field {
  display: block;
  min-width: 0;
}
.family-pane :where(.ui-input, .ui-select, .ui-textarea) {
  display: block;
  min-width: 0;
  max-width: 100%;
  box-sizing: border-box;
}
.family-member-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--ui-s-3);
  padding: 11px 12px;
}
.family-member-row__actions {
  display: flex;
  flex: none;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--ui-s-2);
}
.family-manage-link { margin-top: var(--ui-s-4); }
.family-consent-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--ui-s-3);
  color: var(--ui-text-secondary);
  line-height: 1.5;
}
.family-consent-row input { margin-top: 2px; }
.family-chip-list { display: flex; flex-wrap: wrap; gap: 8px; }
.family-copy-link { overflow-wrap: anywhere; user-select: all; }
.family-onboarding-card { max-width: 560px; margin: var(--ui-s-6) auto; }
.ui-shell--aside { /* semantic alias for the standard 340px aside shell */ }
.ui-help { display: block; margin-top: var(--ui-s-2); color: var(--ui-text-muted); font-size: .75rem; line-height: 1.5; }

@media (max-width: 780px) {
  .family-hero { align-items: stretch; flex-direction: column; }
  .family-hero-stats { width: 100%; min-width: 0; }
  .family-hero-stats span { padding: 12px 6px; }
  .family-tabs { margin-inline: -6px; border-radius: var(--ui-r-md); }
  .family-pane .ui-card__head,
  .family-pane .ui-section-head {
    flex-wrap: wrap;
  }
}

@media (max-width: 520px) {
  .family-hero-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .family-member-row { grid-template-columns: auto minmax(0, 1fr); }
  .family-member-row__actions {
    grid-column: 2;
    justify-content: flex-start;
  }
  .family-pane .ui-card__head > :where(.ui-btn, .ui-badge),
  .family-pane .ui-section-head > :where(.ui-btn, .ui-badge) {
    flex: 0 0 auto;
  }
}

/* ── LIGHT (default) ── */
/*
 * THE CREST'S ROOM, EVERYWHERE.
 *
 * The profile was rebuilt as the Crest — warm paper by day, deep plum at
 * night, with the household's emblem in gold — and every other page stayed on
 * the slate-and-white it had before. Moving between them read as moving
 * between two products, and the one people were proudest of was the one that
 * looked like a guest.
 *
 * So the ground and the ink are the crest's, app-wide, and they are the SAME
 * values as --cr-bg / --cr-surface / --cr-ink below: one palette with two
 * sets of names, not two palettes that have to be kept in step by hand.
 *
 * What did NOT change is the accent. Indigo still means "press this"; gold
 * still means the household and anything waiting on you. Making every button
 * gold would have spent the one colour that carries a meaning here — the
 * "Tobi is waiting" card is unmissable because nothing else on the page is
 * allowed to be gold.
 */
:root,
html[data-ui-theme="light"] {
    color-scheme: light;

    /* Surfaces — 0 is the page, higher numbers sit closer to the viewer */
    --ui-surface-0: #f6f1e7;
    --ui-surface-1: #fffdf8;
    --ui-surface-2: #fffdf8;
    --ui-surface-3: rgba(36, 28, 51, 0.05);
    --ui-surface-inset: #f1e9da;

    --ui-border: rgba(36, 28, 51, 0.11);
    --ui-border-strong: rgba(36, 28, 51, 0.20);

    --ui-text: #241c33;
    --ui-text-secondary: #554b6b;
    --ui-text-muted: #6f6584;
    --ui-text-faint: #8d84a1;

    --ui-accent: #4f46e5;
    --ui-accent-hover: #4338ca;
    --ui-accent-text: #4338ca;
    --ui-accent-soft: rgba(79, 70, 229, 0.09);
    --ui-accent-line: rgba(79, 70, 229, 0.22);

    --ui-success-text: #047857;
    --ui-success-soft: rgba(16, 185, 129, 0.11);
    --ui-warning-text: #b45309;
    --ui-warning-soft: rgba(245, 158, 11, 0.14);
    --ui-danger-text: #b91c1c;
    --ui-danger-soft: rgba(239, 68, 68, 0.08);
    --ui-danger-line: rgba(239, 68, 68, 0.22);

    /* A shadow is the paper's own shade, so it carries the paper's hue.
       Slate shadows on a warm ground read as grey smudges. */
    --ui-shadow-sm: 0 1px 2px rgba(36, 28, 51, 0.05);
    --ui-shadow-md: 0 2px 8px rgba(36, 28, 51, 0.05), 0 8px 24px rgba(36, 28, 51, 0.05);
    --ui-shadow-lg: 0 8px 24px rgba(36, 28, 51, 0.08), 0 24px 56px rgba(36, 28, 51, 0.08);
    --ui-shadow-accent: 0 4px 14px rgba(79, 70, 229, 0.20);

    /* Ink surfaces — deliberately dark panels that stay dark in BOTH modes
       (hero, spotlight). Their own text tokens travel with them.

       These are the jewel: the same deep plum the crest is struck on, so a
       hero on the home page and the sigil panel on the profile are the same
       object seen twice, rather than two dark panels that happen to be dark. */
    --ui-ink: #120e24;
    --ui-ink-2: rgba(244, 239, 228, 0.08);
    --ui-ink-text: #f4efe4;
    --ui-ink-muted: #a79cc4;
    --ui-ink-border: rgba(244, 239, 228, 0.14);
}

/* ── DARK ── */
html[data-ui-theme="dark"] {
    color-scheme: dark;

    --ui-surface-0: #161129;
    --ui-surface-1: #201839;
    --ui-surface-2: #2a2049;
    --ui-surface-3: rgba(244, 239, 228, 0.06);
    --ui-surface-inset: #120e24;

    --ui-border: rgba(244, 239, 228, 0.11);
    --ui-border-strong: rgba(244, 239, 228, 0.20);

    --ui-text: #f4efe4;
    --ui-text-secondary: #c9bfe0;
    --ui-text-muted: #a79cc4;
    --ui-text-faint: #8177a6;

    --ui-accent: #6366f1;
    --ui-accent-hover: #818cf8;
    --ui-accent-text: #a5b4fc;
    --ui-accent-soft: rgba(99, 102, 241, 0.16);
    --ui-accent-line: rgba(99, 102, 241, 0.34);

    --ui-success-text: #6ee7b7;
    --ui-success-soft: rgba(16, 185, 129, 0.15);
    --ui-warning-text: #fcd34d;
    --ui-warning-soft: rgba(245, 158, 11, 0.15);
    --ui-danger-text: #fca5a5;
    --ui-danger-soft: rgba(239, 68, 68, 0.13);
    --ui-danger-line: rgba(239, 68, 68, 0.28);

    --ui-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.40);
    --ui-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.35), 0 8px 24px rgba(0, 0, 0, 0.30);
    --ui-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45), 0 24px 56px rgba(0, 0, 0, 0.40);
    --ui-shadow-accent: 0 4px 14px rgba(99, 102, 241, 0.30);

    /* In dark mode the ink surface lifts slightly so it still separates
       from the page behind it — the jewel one step above the night. */
    --ui-ink: #2a2049;
    --ui-ink-2: rgba(244, 239, 228, 0.06);
    --ui-ink-text: #f4efe4;
    --ui-ink-muted: #c9bfe0;
    --ui-ink-border: rgba(244, 239, 228, 0.10);
}

/* Respect the OS when the page hasn't resolved a preference yet. */
@media (prefers-color-scheme: dark) {
    html:not([data-ui-theme]) body[data-ui-theme-auto] {
        color-scheme: dark;
    }
}


/* ─── 1b. ACCENT ────────────────────────────────────────────────────── */
/* One accent for the whole product. There is no per-portal colour any more:
   Family, Couple, Junior and Student are the same product wearing the same
   clothes, and the only visual switch is light vs dark.

   The `ui-tone-*` class names survive across the codebase but are deliberately
   inert — every one of them resolves to the single accent below. Keeping them
   inert rather than deleting them from ~40 files means no element can end up
   unstyled because a class was missed, and re-introducing accent variation
   later is a change in this one block. */

:root {
    --ui-tone: var(--ui-accent);
    --ui-tone-text: var(--ui-accent-text);
    --ui-tone-soft: var(--ui-accent-soft);
    --ui-tone-line: var(--ui-accent-line);
}

[class*="ui-tone-"] {
    --ui-tone: var(--ui-accent);
    --ui-tone-text: var(--ui-accent-text);
    --ui-tone-soft: var(--ui-accent-soft);
    --ui-tone-line: var(--ui-accent-line);
}


/* ─── 1c. PAGE BASE ─────────────────────────────────────────────────── */
/* v2 pages own their own base styling so they don't depend on whatever
   legacy body rules a given page happens to inherit. */

body[data-ui="v2"] {
    margin: 0;
    min-height: 100vh;
    background: var(--ui-surface-0);
    color: var(--ui-text);
    font-family: 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    overflow-x: hidden;
    transition: background-color var(--ui-base) var(--ui-ease),
                color var(--ui-base) var(--ui-ease);
}

/* Junior keeps Nunito — the one place a rounder voice earns its keep. */
body[data-ui="v2"][data-portal="junior"] {
    font-family: 'Nunito', ui-rounded, ui-sans-serif, system-ui, sans-serif;
}

body[data-ui="v2"] img { max-width: 100%; }

/* ── A person's ⋯ and their sheet ───────────────────────────────────── */
.cr-pmenu {
    flex: none;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: 1px solid var(--cr-line);
    background: var(--cr-surface);
    color: var(--cr-ink-3);
    font-size: 1.0625rem;
    line-height: 1;
    cursor: pointer;
}
.cr-pmenu:hover { background: var(--cr-accent-soft); border-color: var(--cr-accent-line); color: var(--cr-accent); }

/* ── A child who is not in yet ────────────────────────────────────────
 *
 * The same row as everyone else, so the household reads as one list. The
 * ring around the picture and the two controls are the only difference,
 * and they are the whole message: this one is not in yet.
 *
 * The controls take the slots the members use — Approve where their XP
 * would be, decline where their ⋯ would be — so nothing shifts between a
 * row that is settled and a row that is not.
 */
/* The ring is the ROW's, not the avatar's: it is the row that is not settled
   yet, and ringing the picture said something about the child instead. */
.cr-row--wait {
    border-color: var(--cr-accent);
    background: var(--cr-accent-soft);
    box-shadow: 0 0 0 3px var(--cr-accent-soft);
}

/* A person's row carries two controls, so its description gets one line and
   an ellipsis rather than wrapping under them — at 320px "Admin · you" broke
   across two lines and the waiting row's line across three, leaving one row
   in the list half as tall again as the others. Descriptive rows elsewhere
   still wrap, because there the second line is the point. */
.cr-row--person small { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Declining somebody's child is not a thing to make as easy as approving
   it, so it is the quiet ✕ and not a second button of equal weight. */
.cr-x--decline { background: transparent; color: var(--cr-ink-3); }
.cr-x--decline:hover { background: var(--cr-danger-soft); border-color: transparent; color: var(--cr-danger); }

/* The count beside the heading. It is a way into the same rows, so it is
   a control, not the plain chip it sits next to elsewhere. */
.cr-waitchip {
    padding: 4px 11px;
    border-radius: 99px;
    background: var(--cr-accent-soft);
    border: 1px solid var(--cr-accent-line);
    color: var(--cr-accent);
}
.cr-waitchip:hover { background: var(--cr-accent); color: var(--cr-bg); border-color: transparent; }

/* Two mutually exclusive answers, side by side — not a toggle. A chime that
   this browser cannot give is a third state, so the pair is replaced by a
   sentence rather than shown switched off, which would read as a choice
   somebody had made. */
.cr-choice { display: flex; gap: 8px; }
.cr-choice__b {
    flex: 1 1 0;
    min-width: 0;
    min-height: 44px;
    border-radius: 99px;
    border: 1px solid var(--cr-line);
    background: var(--cr-surface);
    color: var(--cr-ink);
    font: inherit;
    font-size: 0.9375rem;
    font-weight: 600;
    cursor: pointer;
}
.cr-choice__b[aria-pressed="true"] {
    background: linear-gradient(160deg, var(--cr-gold-2), var(--cr-gold));
    border-color: transparent;
    color: #2a2012;
    font-weight: 700;
}
.cr-row--danger p { color: var(--cr-danger); }

/* The sheet is titled with their name, so the body carries what the title
   cannot: what they are here, and what they have earned. */
.cr-psheet { display: flex; align-items: center; gap: 13px; }
.cr-psheet > * { min-width: 0; }
.cr-psheet__b { flex: 1 1 auto; min-width: 0; }
.cr-psheet__role { margin: 0; font-size: 0.8125rem; font-weight: 600; color: var(--cr-ink-3); }
.cr-psheet__xp {
    margin: 3px 0 0;
    font-family: Fraunces, Georgia, serif;
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--cr-accent);
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
}

@media (prefers-reduced-motion: reduce) {
    body[data-ui="v2"] * {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }
}


/* ─── 2. TYPOGRAPHY ─────────────────────────────────────────────────── */
/* Two weights carry the whole system: 700 for headings, 500 for body.
   The old design used font-black on everything, which flattened hierarchy. */

.ui-display,
.ui-h1, .ui-h2, .ui-h3, .ui-h4 {
    color: var(--ui-text);
    letter-spacing: -0.02em;
    margin: 0;
    text-wrap: balance;
}

.ui-display { font-size: clamp(2rem, 1.4rem + 2.6vw, 3rem); line-height: 1.06; font-weight: 800; letter-spacing: -0.035em; }
.ui-h1      { font-size: clamp(1.6rem, 1.25rem + 1.4vw, 2.125rem); line-height: 1.15; font-weight: 800; }
.ui-h2      { font-size: 1.375rem; line-height: 1.25; font-weight: 700; }
.ui-h3      { font-size: 1.0625rem; line-height: 1.35; font-weight: 700; }
.ui-h4      { font-size: 0.9375rem; line-height: 1.4; font-weight: 700; }

.ui-lead { font-size: 1rem;     line-height: 1.65; font-weight: 400; color: var(--ui-text-secondary); margin: 0; max-width: 60ch; }
.ui-body { font-size: 0.9375rem; line-height: 1.65; font-weight: 400; color: var(--ui-text-secondary); margin: 0; }
.ui-text-sm  { font-size: 0.875rem;  line-height: 1.55; color: var(--ui-text-secondary); }
.ui-text-xs  { font-size: 0.8125rem; line-height: 1.5;  color: var(--ui-text-muted); }
.ui-muted    { color: var(--ui-text-muted); }
.ui-faint    { color: var(--ui-text-faint); }
.ui-strong   { color: var(--ui-text); font-weight: 600; }

/* Micro-label above a heading. Replaces the old text-[9px] font-black soup. */
.ui-eyebrow {
    display: block;
    font-size: 0.6875rem;
    font-weight: 700;
    line-height: 1.4;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ui-text-muted);
    margin: 0;
}

.ui-numeric { font-variant-numeric: tabular-nums; }


/* ─── 3. LAYOUT PRIMITIVES ──────────────────────────────────────────── */

.ui-page {
    width: 100%;
    max-width: 1280px;
    margin-inline: auto;
    padding-inline: var(--ui-s-4);
    padding-block: var(--ui-s-6) var(--ui-s-16);
}

@media (min-width: 640px)  { .ui-page { padding-inline: var(--ui-s-6); } }
@media (min-width: 1024px) { .ui-page { padding-inline: var(--ui-s-8); } }

.ui-page--narrow { max-width: 720px; }

/* Vertical rhythm without a wrapper div per gap */
/*
 * The stacks space SIBLINGS, so anything that is a sibling but occupies no
 * space still counts as one — and the element after it gets a gap measured
 * from nothing.
 *
 * That is what put an empty band above the first card on /groups and on a
 * group page: the skeletons lead with a visually-hidden `role="status"`
 * announcement, which is 1px and absolutely positioned but is still child
 * one. Child two therefore picked up `margin-top: var(--ui-s-10)` and the
 * left column started 40px below the sidebar.
 *
 * `.hidden` was already excluded for exactly this reason. `.ui-sr-only` is
 * the same case and was missed: an element that exists only for a screen
 * reader must never move anything a sighted reader can see. Fixed here
 * rather than in the skeletons, because any live region, any status message
 * and any skip link dropped into a stack would have hit it.
 */
.ui-stack > :where(:not(.hidden):not([hidden]):not(.ui-sr-only)) ~ :where(:not(.hidden):not([hidden]):not(.ui-sr-only)) {
    margin-top: var(--ui-s-4);
}
.ui-stack-sm > :where(:not(.hidden):not([hidden]):not(.ui-sr-only)) ~ :where(:not(.hidden):not([hidden]):not(.ui-sr-only)) {
    margin-top: var(--ui-s-2);
}
.ui-stack-lg > :where(:not(.hidden):not([hidden]):not(.ui-sr-only)) ~ :where(:not(.hidden):not([hidden]):not(.ui-sr-only)) {
    margin-top: var(--ui-s-6);
}
.ui-stack-xl > :where(:not(.hidden):not([hidden]):not(.ui-sr-only)) ~ :where(:not(.hidden):not([hidden]):not(.ui-sr-only)) {
    margin-top: var(--ui-s-10);
}

.ui-row {
    display: flex;
    align-items: center;
    gap: var(--ui-s-3);
}
.ui-row--between { justify-content: space-between; }
.ui-row--wrap    { flex-wrap: wrap; }
.ui-spacer       { flex: 1 1 auto; }

.ui-grid { display: grid; gap: var(--ui-s-4); }
.ui-grid--2 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/* Always exactly two columns above 560px. Use this for an even number of
   equally-important tiles — auto-fit would leave a 3+1 orphan row. */
.ui-grid--pair { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 560px) {
    .ui-grid--pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.ui-grid--3 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.ui-grid--4 { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }

/* Main content + sidebar. Collapses to one column under 1024px. */
.ui-shell {
    display: grid;
    gap: var(--ui-s-6);
    align-items: start;
}
/*
 * min-width: 0 on every column. This is not optional.
 *
 * A grid item's default minimum is `auto` — its CONTENT width — so anything
 * inside that is wider than the screen makes the column wider than the screen
 * rather than scrolling or wrapping. The tab rail (overflow-x: auto) does
 * exactly that: it wants to be as wide as its tabs, and without this it drags
 * the whole page sideways.
 *
 * It was masked above 1024px because the two-column template below declares
 * `minmax(0, 1fr)`, and that explicit 0 is the same fix. Below the breakpoint
 * there is no template, so the default came back and only mobile broke.
 */
.ui-shell > * { min-width: 0; }
@media (min-width: 1024px) {
    .ui-shell { grid-template-columns: minmax(0, 1fr) 340px; }
    .ui-shell--wide-aside { grid-template-columns: minmax(0, 1fr) 380px; }

    /* Collapse the sidebar column entirely when the aside is hidden, so a
       logged-out visitor doesn't stare at 340px of dead space. */
    .ui-shell:has(> [data-aside].hidden),
    .ui-shell:has(> [data-aside].ui-hidden) {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Centred single-purpose panel — sign-in, empty account, confirmation */
.ui-panel-center {
    max-width: 480px;
    margin-inline: auto;
    margin-block: clamp(var(--ui-s-8), 6vw, var(--ui-s-16));
}

.ui-signin__lead {
    margin-inline: auto;
}

.ui-signin__actions {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--ui-s-3);
    margin-top: var(--ui-s-6);
}

.ui-divider {
    height: 1px;
    background: var(--ui-border);
    border: 0;
    margin-block: var(--ui-s-4);
}


/* ─── 4. APP HEADER ─────────────────────────────────────────────────── */

.ui-appbar {
    position: sticky;
    top: 0;
    z-index: 60;
    background: color-mix(in srgb, var(--ui-surface-0) 82%, transparent);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border-bottom: 1px solid var(--ui-border);
}

.ui-appbar__inner {
    max-width: 1280px;
    margin-inline: auto;
    padding: var(--ui-s-3) var(--ui-s-4);
    display: flex;
    align-items: center;
    gap: var(--ui-s-4);
}
@media (min-width: 640px)  { .ui-appbar__inner { padding-inline: var(--ui-s-6); } }
@media (min-width: 1024px) { .ui-appbar__inner { padding-inline: var(--ui-s-8); } }

.ui-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-s-3);
    text-decoration: none;
    color: inherit;
    border-radius: var(--ui-r-md);
}

/* The brand is the wordmark. The coloured tile and tagline that used to sit
   here were decoration, not identity. */
.ui-brand__mark, .ui-brand__tag { display: none; }

.ui-brand__name {
    font-size: 1.0625rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--ui-text);
    line-height: 1.1;
}


/* Small square control — theme toggle, close, overflow menu */
.ui-iconbtn {
    width: 38px;
    height: 38px;
    flex: none;
    display: inline-grid;
    place-items: center;
    border-radius: var(--ui-r-md);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
    color: var(--ui-text-secondary);
    font-size: 0.9375rem;
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease);
}
.ui-iconbtn:hover {
    background: var(--ui-accent-soft);
    border-color: var(--ui-accent-line);
    color: var(--ui-text);
}


/* ─── 5. SURFACES ───────────────────────────────────────────────────── */

.ui-card {
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-xl);
    box-shadow: var(--ui-shadow-md);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    padding: var(--ui-s-5);
    color: var(--ui-text-secondary);
}

@media (min-width: 640px) { .ui-card { padding: var(--ui-s-6); } }

.ui-card--flush   { padding: 0; overflow: hidden; }
.ui-card--tight   { padding: var(--ui-s-4); }
.ui-card--flat    { box-shadow: none; }
.ui-card--full    { height: 100%; display: flex; flex-direction: column; }
/* Gives a scrolling card a sensible floor without a magic fixed height */
.ui-card--minh    { min-height: 320px; }

/* Card header: title on the left, meta/actions on the right */
.ui-card__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-3);
    padding-bottom: var(--ui-s-3);
    margin-bottom: var(--ui-s-4);
    border-bottom: 1px solid var(--ui-border);
}
.ui-card__title {
    font-size: 0.875rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--ui-text);
    margin: 0;
}
/* Optional one-line explanation under a card title. The head is a flex row,
   so the title and this need a wrapper <div> between them — see the pattern
   already used by .ui-section-head. */
.ui-card__sub {
    margin: 3px 0 0;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--ui-text-muted);
    font-weight: 500;
}

/* Clickable card — used for the game picker */
.ui-card-select {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--ui-s-3);
    width: 100%;
    text-align: left;
    padding: var(--ui-s-5);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-xl);
    box-shadow: var(--ui-shadow-sm);
    cursor: pointer;
    overflow: hidden;
    isolation: isolate;
    transition: border-color var(--ui-base) var(--ui-ease),
                transform var(--ui-base) var(--ui-ease),
                box-shadow var(--ui-base) var(--ui-ease);
}

/* Accent wash that grows on hover — replaces the old absolutely-positioned blob */
.ui-card-select::before {
    content: "";
    position: absolute;
    inset: auto -30% -60% auto;
    width: 220px;
    height: 220px;
    border-radius: 50%;
    background: var(--ui-tone-soft);
    opacity: 0;
    transform: scale(0.6);
    transition: opacity var(--ui-base) var(--ui-ease), transform var(--ui-base) var(--ui-ease);
    z-index: -1;
}

.ui-card-select:hover {
    border-color: color-mix(in srgb, var(--ui-tone) 45%, transparent);
    transform: translateY(-3px);
    box-shadow: var(--ui-shadow-lg);
}
.ui-card-select:hover::before { opacity: 1; transform: scale(1); }
.ui-card-select:active { transform: translateY(-1px); }

.ui-card-select__icon {
    width: 46px;
    height: 46px;
    display: grid;
    place-items: center;
    font-size: 1.5rem;
    line-height: 1;
    border-radius: var(--ui-r-md);
    background: var(--ui-tone-soft);
    border: 1px solid color-mix(in srgb, var(--ui-tone) 28%, transparent);
}

/*
 * Row variant: icon on the left, text stacked beside it.
 *
 * The tool rail on Home lists many short entries — a title and one line of
 * status — where the default stacked card wastes a full icon row of height
 * per item. `__body` holds the stacked text and is allowed to shrink so a
 * long tool name ellipses instead of pushing the card wide.
 */
.ui-card-select--row {
    flex-direction: row;
    align-items: center;
    gap: var(--ui-s-4);
}

.ui-card-select--row .ui-card-select__icon { flex: 0 0 auto; }

.ui-card-select__body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.ui-card-select__body > * {
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-card-select__cta {
    margin-top: auto;
    padding-top: var(--ui-s-2);
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-tone-text);
}
.ui-card-select__cta::after {
    content: "→";
    transition: transform var(--ui-base) var(--ui-ease);
}
.ui-card-select:hover .ui-card-select__cta::after { transform: translateX(4px); }


/* ─── 6. BUTTONS ────────────────────────────────────────────────────── */

.ui-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ui-s-2);
    padding: 10px 18px;
    font-family: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: -0.005em;
    border-radius: var(--ui-r-md);
    border: 1px solid transparent;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease),
                box-shadow var(--ui-fast) var(--ui-ease),
                transform var(--ui-fast) var(--ui-ease);
}
.ui-btn:active { transform: translateY(1px); }

.ui-btn:disabled,
.ui-btn[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
    transform: none;
}

.ui-btn--primary {
    background: var(--ui-accent);
    color: var(--ui-on-accent);
    box-shadow: var(--ui-shadow-accent);
}
.ui-btn--primary:hover:not(:disabled) { background: var(--ui-accent-hover); }

.ui-btn--secondary {
    background: var(--ui-surface-3);
    border-color: var(--ui-border);
    color: var(--ui-text);
}
.ui-btn--secondary:hover:not(:disabled) {
    background: var(--ui-accent-soft);
    border-color: var(--ui-accent-line);
}

.ui-btn--ghost {
    background: transparent;
    color: var(--ui-text-muted);
}
.ui-btn--ghost:hover:not(:disabled) {
    background: var(--ui-surface-3);
    color: var(--ui-text);
}

.ui-btn--danger {
    background: var(--ui-danger-soft);
    border-color: var(--ui-danger-line);
    color: var(--ui-danger-text);
}
.ui-btn--danger:hover:not(:disabled) {
    background: color-mix(in srgb, var(--ui-danger) 22%, transparent);
}

.ui-btn--sm    { padding: 7px 13px; font-size: 0.8125rem; border-radius: var(--ui-r-sm); }
.ui-btn--lg    { padding: 13px 26px; font-size: 0.9375rem; border-radius: var(--ui-r-lg); }
.ui-btn--block { width: 100%; }

/* Quiet inline link-button, e.g. "Back to games" */
.ui-btn-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-accent-text);
    cursor: pointer;
    transition: color var(--ui-fast) var(--ui-ease);
}
.ui-btn-link:hover { color: var(--ui-text); }


/* ─── 7. FORMS ──────────────────────────────────────────────────────── */

.ui-field { display: block; }

.ui-label {
    display: block;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text-secondary);
    margin-bottom: 6px;
}

.ui-input,
.ui-select,
.ui-textarea {
    width: 100%;
    padding: 10px 14px;
    font-family: inherit;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.45;
    color: var(--ui-text);
    background: var(--ui-surface-inset);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-md);
    outline: none;
    transition: border-color var(--ui-fast) var(--ui-ease), box-shadow var(--ui-fast) var(--ui-ease);
}

.ui-input::placeholder,
.ui-textarea::placeholder { color: var(--ui-text-faint); }

/* A textarea is inline-block by default, which leaves a baseline gap under it
   and lets the browser's intrinsic `cols` width fight the declared 100%.
   Vertical resize only: dragging one wider than its card is never wanted. */
.ui-textarea {
    display: block;
    resize: vertical;
    min-height: 92px;
    max-width: 100%;
}

.ui-input:focus,
.ui-select:focus,
.ui-textarea:focus {
    border-color: var(--ui-accent);
    box-shadow: 0 0 0 3px var(--ui-accent-soft);
}

.ui-hint { font-size: 0.75rem; color: var(--ui-text-muted); margin-top: 6px; }

/* One visible focus style for the whole app */
:where(.ui-btn, .ui-btn-link, .ui-iconbtn, .ui-card-select, .ui-brand):focus-visible {
    outline: 2px solid var(--ui-accent);
    outline-offset: 2px;
}


/* ─── 8. BADGES, CHIPS, STATUS ──────────────────────────────────────── */

.ui-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 9px;
    font-size: 0.6875rem;
    font-weight: 600;
    line-height: 1.5;
    letter-spacing: 0.03em;
    border-radius: var(--ui-r-full);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    color: var(--ui-text-secondary);
    white-space: nowrap;
}

.ui-badge--accent  { background: var(--ui-accent-soft);  border-color: var(--ui-accent-line);  color: var(--ui-accent-text); }
.ui-badge--tone    { background: var(--ui-tone-soft);    border-color: color-mix(in srgb, var(--ui-tone) 30%, transparent); color: var(--ui-tone-text); }
.ui-badge--success { background: var(--ui-success-soft);  border-color: color-mix(in srgb, var(--ui-success) 30%, transparent); color: var(--ui-success-text); }
.ui-badge--warning { background: var(--ui-warning-soft);  border-color: color-mix(in srgb, var(--ui-warning) 30%, transparent); color: var(--ui-warning-text); }
.ui-badge--danger  { background: var(--ui-danger-soft);   border-color: var(--ui-danger-line); color: var(--ui-danger-text); }

.ui-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--ui-success);
    flex: none;
}
.ui-dot--live { animation: ui-pulse 2s var(--ui-ease) infinite; }

@keyframes ui-pulse {
    0%, 100% { opacity: 1;   transform: scale(1); }
    50%      { opacity: 0.45; transform: scale(1.35); }
}


/* ─── 9. LISTS & AVATARS ────────────────────────────────────────────── */

.ui-list { display: flex; flex-direction: column; gap: 6px; }

.ui-list-row {
    display: flex;
    align-items: center;
    gap: var(--ui-s-3);
    padding: 8px 10px;
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    transition: background var(--ui-fast) var(--ui-ease), border-color var(--ui-fast) var(--ui-ease);
}
.ui-list-row:hover { background: var(--ui-accent-soft); border-color: var(--ui-accent-line); }

.ui-list-row__body { flex: 1 1 auto; min-width: 0; }
.ui-list-row__name {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text);
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ui-list-row__meta { font-size: 0.6875rem; color: var(--ui-text-muted); margin: 1px 0 0; }

.ui-avatar {
    width: 34px;
    height: 34px;
    flex: none;
    border-radius: var(--ui-r-sm);
    overflow: hidden;
    display: grid;
    place-items: center;
    background: var(--ui-surface-2);
    border: 1px solid var(--ui-border);
    font-size: 0.9375rem;
}
.ui-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ui-avatar--sm { width: 26px; height: 26px; font-size: 0.8125rem; }
.ui-avatar--lg { width: 56px; height: 56px; border-radius: var(--ui-r-md); font-size: 1.5rem; }

/* Numbered explainer list — replaces "1." typed into a <strong> tag */
.ui-steps {
    list-style: none;
    counter-reset: ui-step;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--ui-s-4);
}
.ui-steps > li {
    position: relative;
    padding-left: 32px;
    font-size: 0.8125rem;
    line-height: 1.6;
    color: var(--ui-text-secondary);
}
.ui-steps > li::before {
    counter-increment: ui-step;
    content: counter(ui-step);
    position: absolute;
    left: 0;
    top: 1px;
    width: 22px;
    height: 22px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--ui-accent-soft);
    border: 1px solid var(--ui-accent-line);
    color: var(--ui-accent-text);
    font-size: 0.6875rem;
    font-weight: 700;
}
.ui-steps > li > strong { display: block; margin-bottom: 1px; }

/* Section label inside a scrolling list */
.ui-list-label {
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ui-text-faint);
    margin: var(--ui-s-3) 0 var(--ui-s-2);
}
.ui-list-label:first-child { margin-top: 0; }


/* ─── 10. EMPTY STATES & SCROLL AREAS ───────────────────────────────── */

.ui-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    text-align: center;
    padding: var(--ui-s-8) var(--ui-s-4);
    color: var(--ui-text-muted);
    min-height: 120px;
}
.ui-empty__icon  { font-size: 1.75rem; line-height: 1; opacity: 0.65; }
.ui-empty__title { font-size: 0.875rem; font-weight: 600; color: var(--ui-text-secondary); margin: 0; }
.ui-empty__text  { font-size: 0.8125rem; color: var(--ui-text-muted); margin: 0; max-width: 34ch; line-height: 1.55; }

/* Flexible scroll region: fills its card instead of using a magic pixel height */
.ui-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--ui-border-strong) transparent;
    padding-right: 4px;
}
.ui-scroll::-webkit-scrollbar { width: 6px; }
.ui-scroll::-webkit-scrollbar-track { background: transparent; }
.ui-scroll::-webkit-scrollbar-thumb { background: var(--ui-border-strong); border-radius: var(--ui-r-full); }
.ui-scroll::-webkit-scrollbar-thumb:hover { background: var(--ui-text-faint); }

.ui-scroll--sm { max-height: 200px; }
.ui-scroll--md { max-height: 300px; }

/* On phones, a scrollable list inside a scrollable page makes a swipe feel
   trapped: the list takes the gesture, then its containment prevents the
   page from continuing. Let ordinary lists grow with the page instead.
   Purpose-built scrolling surfaces such as chat and modals stay independent. */
@media (max-width: 639px) {
    .ui-scroll:not(.ui-chatwin__body):not(.ui-modal .ui-scroll) {
        overflow-y: visible;
        overscroll-behavior: auto;
        max-height: none;
        padding-right: 0;
    }
}


/* ─── 11. MODALS ────────────────────────────────────────────────────── */

.ui-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 300;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--ui-s-4);
    background: rgba(15, 23, 42, 0.45);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}
.ui-modal-backdrop.hidden { display: none; }

/* Set on <body> while a modal is open. Without it the page behind scrolls
   under the finger on a phone, and closing the modal returns the reader to
   somewhere they never navigated to. */
body.ui-modal-open { overflow: hidden; }

html[data-ui-theme="dark"] .ui-modal-backdrop { background: rgba(2, 6, 23, 0.76); }

.ui-modal {
    position: relative;
    width: 100%;
    max-width: 420px;
    max-height: calc(100vh - var(--ui-s-8));
    overflow-y: auto;
    padding: var(--ui-s-6);
    background: var(--ui-surface-2);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-2xl);
    box-shadow: var(--ui-shadow-lg);
    animation: ui-modal-in var(--ui-base) var(--ui-ease);
}
.ui-modal--sm { max-width: 340px; }
.ui-modal--lg { max-width: 640px; }
.ui-modal--xl { max-width: 760px; }

/* ══ Sheets ════════════════════════════════════════════════════════════
   A dialog that ARRIVES rather than appears.

   On a phone every one of these is reached with a thumb at the bottom of the
   screen, and a panel that materialises in the middle has no relationship to
   the tap that opened it. Rising from the edge the thumb is already resting
   on does: it says where it came from, and it says how to send it back.

   This started life inside quest-single.html as #rating-modal, which worked
   so well that everything else on that page looked broken beside it -- so it
   lives here now, with the rest of the system, and a page that wants the
   behaviour asks for the classes instead of copying thirty lines of CSS.

   THE SHAPE IS TWO ELEMENTS.

     <div class="ui-sheet-modal hidden">     <- backdrop, owns the fade
       <div class="ui-sheet-modal__panel">   <- panel, owns the slide
     </div>

   THE OPENING IS TWO STEPS, and the second must be one frame later:

     el.classList.remove('hidden');
     requestAnimationFrame(() => el.classList.add('is-open'));

   Without that frame the panel has no start state to travel from and simply
   appears. Closing is the mirror -- drop `is-open`, and add `hidden` only
   once it has finished sliding down (320ms) -- so that leaving is as visible
   as arriving was. `hidden` therefore cannot mean display:none here; it means
   "not interactive", and the pointer-events rule below is what enforces it.

   Above 640px this is a centred dialog again, because a sheet on a desktop
   is a phone gesture performed with a mouse. */
.ui-sheet-modal {
    position: fixed;
    inset: 0;
    z-index: 300;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding: 0;
    background: rgba(15, 23, 42, 0.45);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    opacity: 0;
    pointer-events: none;
    transition: opacity .22s ease;
}
html[data-ui-theme="dark"] .ui-sheet-modal { background: rgba(2, 6, 23, 0.76); }
/* Still laid out while hidden: the panel needs somewhere to slide back TO. */
.ui-sheet-modal.hidden { display: flex; }
.ui-sheet-modal.is-open { opacity: 1; pointer-events: auto; }

.ui-sheet-modal__panel {
    width: 100%;
    max-width: 32rem;
    /* dvh, not vh: with a mobile URL bar showing, 88vh is taller than the
       screen actually is, and the footer goes under it. */
    max-height: 88dvh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--ui-surface-2);
    border: 1px solid var(--ui-border);
    /* Square at the bottom: it is attached to the edge of the screen, not
       floating above it. The desktop rule below rounds it again. */
    border-radius: var(--ui-r-2xl) var(--ui-r-2xl) 0 0;
    box-shadow: var(--ui-shadow-lg);
    transform: translateY(100%);
    transition: transform .32s cubic-bezier(.32, .72, 0, 1);
    /*
     * A REAL gutter, plus whatever the device reserves.
     *
     * env(safe-area-inset-bottom) alone was not enough and on most phones is
     * literally 0: Android's gesture pill and Chrome's floating search bubble
     * are drawn OVER the page and reserve nothing, so the last button in the
     * sheet sat flush on the screen edge with the system UI on top of it --
     * unreadable, and a hair away from a mis-tap into the launcher. The fixed
     * gutter is what makes the sheet look attached to the edge rather than
     * cut off by it; the inset is added on top for the notched devices that
     * do report one.
     */
    padding-bottom: calc(var(--ui-s-6) + env(safe-area-inset-bottom, 0px));
}
.ui-sheet-modal.is-open .ui-sheet-modal__panel { transform: translateY(0); }

.ui-sheet-modal__panel--lg { max-width: 42rem; }

/* A sheet opened FROM another sheet. Rare by design -- stacking dialogs is
   usually a sign the first one asked the wrong question -- but nudging
   somebody is chosen from the participants list, and that list stays open
   behind so you can see who you picked. Without this they share z-index and
   the newer one loses. */
.ui-sheet-modal--over { z-index: 320; }

/* The grip. Not a control -- it is the affordance that says this thing came
   from the bottom and can go back there, which is why it is not focusable. */
.ui-sheet-modal__grip {
    width: 36px;
    height: 4px;
    margin: 10px auto 2px;
    border-radius: 999px;
    background: var(--ui-border);
    flex: none;
}

@media (min-width: 640px) {
    .ui-sheet-modal { align-items: center; padding: var(--ui-s-4); }
    .ui-sheet-modal__panel {
        border-radius: var(--ui-r-2xl);
        transform: translateY(16px) scale(.98);
        /* Centred dialog again: no screen edge to clear, so the panel's own
           padding is the whole of it. */
        padding-bottom: 0;
    }
    .ui-sheet-modal__grip { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .ui-sheet-modal, .ui-sheet-modal__panel { transition: none; }
    .ui-sheet-modal__panel { transform: none; }
}

@keyframes ui-modal-in {
    from { opacity: 0; transform: translateY(12px) scale(0.98); }
    to   { opacity: 1; transform: none; }
}

/* ── Every system modal is a sheet on a phone ──────────────────────────
   The rule above is the desktop one: a panel that settles into the middle
   of a wide screen, where the pointer already is. On a phone the pointer is
   a thumb at the BOTTOM of the screen, and the same panel in the middle has
   no relationship to the tap that opened it -- it is just something that
   appeared.

   So below 640px every .ui-modal-backdrop docks its panel to the bottom
   edge and every .ui-modal rises from it. This is deliberately done in CSS
   on the existing classes rather than by converting each modal by hand:
   openModal()/closeModal() in tools.html, the report sheets, the poll vote
   dialog and everything else that already uses the system get the behaviour
   without a line of JavaScript, and anything written next gets it for free.

   `ui-sheet-modal` (above) remains the richer version, for the sheets whose
   open and close are both animated -- it needs its two-step open, so it is
   opt-in. This is the floor, not the ceiling. */
@media (max-width: 639.98px) {
    .ui-modal-backdrop { align-items: flex-end; padding: 0; }
    .ui-modal {
        max-width: none;
        max-height: 92dvh;
        border-radius: var(--ui-r-2xl) var(--ui-r-2xl) 0 0;
        border-bottom: none;
        /* The home indicator sits under the last button otherwise. */
        padding-bottom: calc(var(--ui-s-6) + env(safe-area-inset-bottom, 0px));
        animation: ui-sheet-in var(--ui-slow, .32s) cubic-bezier(.32, .72, 0, 1);
    }
    .ui-modal--sm, .ui-modal--lg, .ui-modal--xl { max-width: none; }
}

@keyframes ui-sheet-in {
    from { transform: translateY(100%); }
    to   { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .ui-modal { animation: none; }
    .ui-sheet-modal, .ui-sheet-modal__panel { transition: none; }
    .ui-card-select, .ui-btn, .ui-iconbtn { transition: none; }
    .ui-dot--live { animation: none; }
}

.ui-modal__close {
    position: absolute;
    top: var(--ui-s-4);
    right: var(--ui-s-4);
    width: 30px;
    height: 30px;
    display: grid;
    place-items: center;
    border-radius: var(--ui-r-full);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
    color: var(--ui-text-muted);
    font-size: 0.8125rem;
    cursor: pointer;
    transition: color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-modal__close:hover { color: var(--ui-text); background: var(--ui-accent-soft); }

.ui-modal__actions {
    display: flex;
    flex-direction: column;
    gap: var(--ui-s-2);
    margin-top: var(--ui-s-6);
}

.ui-modal__icon {
    font-size: 2.5rem;
    line-height: 1;
}

/* Profile chooser grid — auto-fits so 2 kids don't look lost in a 4-col grid */
.ui-profile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: var(--ui-s-3);
    max-height: 340px;
    overflow-y: auto;
}

.ui-profile-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--ui-s-2);
    padding: var(--ui-s-4);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-lg);
    cursor: pointer;
    transition: border-color var(--ui-fast) var(--ui-ease),
                background var(--ui-fast) var(--ui-ease),
                transform var(--ui-fast) var(--ui-ease);
}
.ui-profile-card:hover {
    background: var(--ui-accent-soft);
    border-color: var(--ui-accent-line);
    transform: translateY(-2px);
}
.ui-profile-card:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }
.ui-profile-card__name { display: block; font-size: 0.875rem; font-weight: 600; color: var(--ui-text); margin: 0; }
.ui-profile-card__role {
    display: block;
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ui-accent-text);
    margin: 2px 0 0;
}

.ui-spinner {
    width: 34px;
    height: 34px;
    margin-inline: auto;
    border: 3px solid var(--ui-accent-soft);
    border-top-color: var(--ui-accent);
    border-radius: 50%;
    animation: ui-spin 700ms linear infinite;
}
@keyframes ui-spin { to { transform: rotate(360deg); } }


/* ─── 12. BANNERS ───────────────────────────────────────────────────── */

.ui-banner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ui-s-4);
    padding: var(--ui-s-4) var(--ui-s-5);
    border-radius: var(--ui-r-lg);
    background: var(--ui-accent-soft);
    border: 1px solid var(--ui-accent-line);
    color: var(--ui-text-secondary);
}
.ui-banner__body { flex: 1 1 240px; min-width: 0; }
.ui-banner__title { font-size: 0.875rem; font-weight: 700; color: var(--ui-text); margin: 0; }
.ui-banner__text  { font-size: 0.8125rem; margin: 2px 0 0; color: var(--ui-text-secondary); }
.ui-banner--success { background: var(--ui-success-soft); border-color: color-mix(in srgb, var(--ui-success) 32%, transparent); }
.ui-banner--warning { background: var(--ui-warning-soft); border-color: color-mix(in srgb, var(--ui-warning) 32%, transparent); }
.ui-banner__icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border-radius: 999px;
    background: var(--ui-surface);
    font-size: 1rem;
}
/* A banner used as a standing notice: tighter than a page-level one, and the
   text carries the weight because there is no title line above it. */
.ui-banner--note { gap: var(--ui-s-3); padding: var(--ui-s-3) var(--ui-s-4); }
.ui-banner--note .ui-banner__text { font-weight: 600; color: var(--ui-text); }


/* ─── 13. FLOATING CHAT ─────────────────────────────────────────────── */

.ui-fab {
    position: fixed;
    right: var(--ui-s-6);
    bottom: var(--ui-s-6);
    z-index: 200;
    width: 52px;
    height: 52px;
    display: grid;
    place-items: center;
    border-radius: var(--ui-r-full);
    border: 1px solid var(--ui-border-strong);
    background: var(--ui-accent);
    color: #fff;
    font-size: 1.25rem;
    cursor: pointer;
    box-shadow: var(--ui-shadow-lg);
    transition: transform var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-fab:hover { background: var(--ui-accent-hover); transform: translateY(-2px); }

.ui-fab__badge {
    position: absolute;
    top: -3px;
    right: -3px;
    min-width: 19px;
    height: 19px;
    padding: 0 5px;
    display: grid;
    place-items: center;
    border-radius: var(--ui-r-full);
    background: var(--ui-danger);
    color: #fff;
    font-size: 0.625rem;
    font-weight: 700;
}

.ui-chatwin {
    position: fixed;
    right: var(--ui-s-6);
    bottom: 88px;
    z-index: 200;
    width: min(340px, calc(100vw - var(--ui-s-8)));
    height: min(440px, calc(100vh - 160px));
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--ui-surface-2);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-xl);
    box-shadow: var(--ui-shadow-lg);
    transform-origin: bottom right;
    transition: opacity var(--ui-base) var(--ui-ease), transform var(--ui-base) var(--ui-ease);
}
.ui-chatwin.hidden { display: none; }
.ui-chatwin.is-closed { opacity: 0; transform: scale(0.94) translateY(8px); }

.ui-chatwin__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-3);
    padding: var(--ui-s-3) var(--ui-s-4);
    border-bottom: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
}
.ui-chatwin__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: var(--ui-s-4);
    display: flex;
    flex-direction: column;
    gap: var(--ui-s-3);
}
.ui-msg {
    display: flex;
    align-items: flex-start;
    gap: var(--ui-s-2);
}
.ui-msg__bubble {
    flex: 1 1 auto;
    min-width: 0;
    padding: 8px 11px;
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
}
.ui-msg--me .ui-msg__bubble {
    background: var(--ui-accent-soft);
    border-color: var(--ui-accent-line);
}
.ui-msg__meta {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--ui-s-2);
    margin-bottom: 3px;
}
.ui-msg__sender { font-size: 0.75rem; font-weight: 600; color: var(--ui-text); }
.ui-msg__time   { font-size: 0.625rem; color: var(--ui-text-faint); flex: none; }
.ui-msg__text   { font-size: 0.8125rem; line-height: 1.5; color: var(--ui-text-secondary); margin: 0; overflow-wrap: anywhere; }

.ui-chatwin__foot {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
    padding: var(--ui-s-3);
    border-top: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
}


/* ─── 14. UTILITIES ─────────────────────────────────────────────────── */

/* ── Visibility ──
   Tailwind's `.hidden` lives in the `utilities` layer. Our component classes
   are unlayered, so anything that sets `display` (.ui-shell, .ui-chatwin,
   .ui-fab__badge …) would silently out-rank it and show while "hidden".
   The whole app toggles visibility with .hidden, so it wins outright. */
.hidden         { display: none !important; }
.ui-hidden      { display: none !important; }

/* Legacy shared headers used `hidden md:flex`, but the intentionally strong
   `.hidden` rule above wins even after the desktop breakpoint. Give those
   headers semantic responsive classes so their links remain reachable. */
.legacy-nav-desktop { display: none; }
.legacy-nav-mobile-toggle { display: inline-flex; }
@media (min-width: 768px) {
    .legacy-nav-desktop { display: flex; }
    .legacy-nav-mobile-toggle { display: none; }
}

.ui-center      { text-align: center; }
.ui-truncate    { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-mt-auto     { margin-top: auto; }
.ui-full        { width: 100%; }
.ui-grow        { flex: 1 1 0%; }

.ui-mt-1 { margin-top: var(--ui-s-1); }
.ui-mt-2 { margin-top: var(--ui-s-2); }
.ui-mt-3 { margin-top: var(--ui-s-3); }
.ui-mt-4 { margin-top: var(--ui-s-4); }
.ui-mb-4 { margin-bottom: var(--ui-s-4); }

/* Screen-reader-only text — for icon-only buttons */
.ui-sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}


/* ══════════════════════════════════════════════════════════════════════
   15. PORTAL SHELL
   ----------------------------------------------------------------------
   Everything below is shared by family / couple / junior. The only thing
   that differs between the three portals is one `ui-tone-*` class on
   <body>. Nothing here hard-codes a portal colour.
   ══════════════════════════════════════════════════════════════════════ */

/* ─── Navigation ────────────────────────────────────────────────────── */

.ui-nav {
    position: sticky;
    top: 0;
    z-index: 80;
    background: color-mix(in srgb, var(--ui-surface-0) 86%, transparent);
    backdrop-filter: blur(16px) saturate(1.4);
    -webkit-backdrop-filter: blur(16px) saturate(1.4);
    border-bottom: 1px solid var(--ui-border);
}

.ui-nav__inner {
    max-width: 1200px;
    margin-inline: auto;
    padding: 0 var(--ui-s-4);
    height: 64px;
    display: flex;
    align-items: center;
    gap: var(--ui-s-4);
}
@media (min-width: 640px)  { .ui-nav__inner { padding-inline: var(--ui-s-6); } }
@media (min-width: 1024px) { .ui-nav__inner { padding-inline: var(--ui-s-8); } }

.ui-nav__brand {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    text-decoration: none;
    color: inherit;
    border-radius: var(--ui-r-md);
    flex: none;
}

/* The logo is the wordmark. It previously sat in a coloured rounded tile with
   an emoji inside, which competed with the page for attention and read as
   decoration rather than identity. */
.ui-nav__logo { display: none; }

.ui-nav__word {
    font-size: 1.125rem;
    font-weight: 700;
    letter-spacing: -0.025em;
    color: var(--ui-text);
    white-space: nowrap;
}
.ui-nav__word b { font-weight: 700; color: var(--ui-tone-text); }

/* Desktop links */
.ui-nav__links {
    display: none;
    align-items: center;
    gap: 2px;
    margin-inline: auto;
}
@media (min-width: 900px) { .ui-nav__links { display: flex; } }

.ui-nav__link {
    position: relative;
    padding: 8px 14px;
    border-radius: var(--ui-r-md);
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--ui-text-muted);
    text-decoration: none;
    white-space: nowrap;
    transition: color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-nav__link:hover { color: var(--ui-text); background: var(--ui-surface-3); }

.ui-nav__link[aria-current="page"] {
    color: var(--ui-tone-text);
    background: var(--ui-tone-soft);
}

.ui-nav__notifications {
    display: flex;
    align-items: center;
    margin-left: auto;
}
@media (min-width: 900px) { .ui-nav__notifications { margin-left: auto; } }

.ui-nav__actions {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
    margin-left: var(--ui-s-2);
}

/* ─── Account menu (the hamburger right of the avatar) ─────────────────
   The burger is a DESKTOP control. Below 900px the bottom bar carries
   navigation and its "More" sheet carries the account items, so a second
   menu in the header would be two ways to reach the same four links —
   and the header is where the smallest tap targets used to live. */
.ui-nav__menu-wrap { position: relative; display: flex; align-items: center; }

.ui-nav__burger { display: none; }
@media (min-width: 900px) { .ui-nav__burger { display: inline-grid; } }

.ui-nav__menu {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 200;
    min-width: 200px;
    padding: var(--ui-s-2);
    display: flex;
    flex-direction: column;
    gap: 2px;
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-lg);
    box-shadow: var(--ui-shadow-lg, 0 16px 40px rgba(15, 23, 42, 0.16));
    transform-origin: top right;
    transition: opacity var(--ui-fast) var(--ui-ease), transform var(--ui-fast) var(--ui-ease);
}

.ui-nav__menu-label {
    padding: 6px 10px 2px;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ui-text-muted);
}

.ui-nav__menu-link {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 10px 10px;
    border: 0;
    border-radius: var(--ui-r-md);
    background: transparent;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    text-align: left;
    text-decoration: none;
    color: var(--ui-text);
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease), color var(--ui-fast) var(--ui-ease);
}
.ui-nav__menu-link:hover { background: var(--ui-surface-3); }
.ui-nav__menu-link[aria-current="page"] { color: var(--ui-tone-text); background: var(--ui-tone-soft); }
.ui-nav__menu-link--danger { color: var(--ui-danger-text, #e11d48); }
.ui-nav__menu-link--danger:hover { background: color-mix(in srgb, #e11d48 12%, transparent); }

.ui-nav__menu-sep {
    height: 1px;
    margin: var(--ui-s-2) 6px;
    background: var(--ui-border);
    border: 0;
}


/* ─── Mobile bottom bar ────────────────────────────────────────────────
   Five destinations, thumb height, always the same five. It replaces the
   header drawer below 900px: a drawer hid navigation behind a tap in the
   furthest corner from the thumb, which is why nobody found Polls. */
.ui-bottomnav {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 90;
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 2px;
    padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px));
    background: color-mix(in srgb, var(--ui-surface-0) 92%, transparent);
    backdrop-filter: blur(16px) saturate(1.4);
    -webkit-backdrop-filter: blur(16px) saturate(1.4);
    border-top: 1px solid var(--ui-border);
}
.ui-bottomnav > * { min-width: 0; }
@media (min-width: 900px) { .ui-bottomnav { display: none; } }

.ui-bottomnav__item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    /* 48px of height plus the label — the WCAG target floor, not a guess. */
    min-height: 48px;
    padding: 4px 2px;
    border: 0;
    border-radius: var(--ui-r-md);
    background: transparent;
    font: inherit;
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    color: var(--ui-text-muted);
    text-decoration: none;
    cursor: pointer;
    transition: color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-bottomnav__item:hover { color: var(--ui-text); }
.ui-bottomnav__item:active { background: var(--ui-surface-3); }
.ui-bottomnav__item[aria-current="page"],
.ui-bottomnav__item[aria-expanded="true"] { color: var(--ui-tone-text); }

.ui-bottomnav__icon {
    display: grid;
    place-items: center;
    width: 22px;
    height: 22px;
}
.ui-bottomnav__icon svg { width: 22px; height: 22px; }

.ui-bottomnav__label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The avatar rides in the More tab, so the person can see whose account
   the sheet is about before opening it. */
.ui-bottomnav__avatar { width: 22px; height: 22px; border-radius: 999px; object-fit: cover; }

/* ─── The More sheet ─────────────────────────────────────────────────── */
.ui-sheet {
    position: fixed;
    inset: 0;
    z-index: 120;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    background: rgba(15, 23, 42, 0.45);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--ui-base) var(--ui-ease);
}
.ui-sheet.is-open { opacity: 1; pointer-events: auto; }
@media (min-width: 900px) { .ui-sheet { display: none; } }

.ui-sheet__panel {
    max-height: 82vh;
    overflow-y: auto;
    padding: var(--ui-s-4) var(--ui-s-4) calc(var(--ui-s-5) + env(safe-area-inset-bottom, 0px));
    background: var(--ui-surface-1);
    border-top-left-radius: var(--ui-r-2xl);
    border-top-right-radius: var(--ui-r-2xl);
    border-top: 1px solid var(--ui-border);
    transform: translateY(12%);
    transition: transform var(--ui-base) var(--ui-ease);
}
.ui-sheet.is-open .ui-sheet__panel { transform: translateY(0); }

.ui-sheet__grip {
    width: 40px; height: 4px;
    margin: 0 auto var(--ui-s-4);
    border-radius: 999px;
    background: var(--ui-border-strong, var(--ui-border));
}

/* Close sits on the same row as the last account action. The first attempt
   was an icon alone, transparent, floating beside the centred grip — nothing
   about it said "button". This one is bordered, filled and labelled, and it
   is anchored to a row instead of hovering in the header. */
.ui-sheet__account-row {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
}
.ui-sheet__account-row > .ui-nav__menu-link { flex: 1 1 auto; min-width: 0; }

.ui-sheet__close {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 10px 16px;
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-2);
    color: var(--ui-text);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease);
}
.ui-sheet__close svg { width: 15px; height: 15px; flex: none; }
.ui-sheet__close:hover { background: var(--ui-surface-3); }
.ui-sheet__close:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }

.ui-sheet__links {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--ui-s-2);
}
.ui-sheet__links > * { min-width: 0; }

.ui-sheet__link {
    display: flex;
    align-items: center;
    min-height: 48px;
    padding: 12px 14px;
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-2);
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--ui-text);
    text-decoration: none;
}
.ui-sheet__link[aria-current="page"] {
    color: var(--ui-tone-text);
    background: var(--ui-tone-soft);
    border-color: var(--ui-tone-soft);
}

.ui-sheet__account { display: flex; flex-direction: column; gap: 2px; }

/*
 * SWITCH THEME — two named answers side by side.
 *
 * This was `.ui-sheet__theme`, one row styled as a link and labelled with the
 * theme you would GET. A control that names its outcome instead of its state
 * cannot be read: you had to look at the page behind the menu to work out
 * which theme you were already in. The pair shows both, one pressed.
 *
 * Same shape as the text-size steps directly beneath it, because they are the
 * same kind of question — pick one of these — and two different shapes for
 * that in one drawer reads as two different kinds of control.
 */
.ui-sheet__choice {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--ui-s-2);
    margin-bottom: var(--ui-s-3);
}
.ui-sheet__choice-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--ui-s-2);
    /* The same 56px floor the text-size steps use (UI-STANDARD.md §3). */
    min-height: 56px;
    padding: var(--ui-s-2) var(--ui-s-3);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-2);
    color: var(--ui-text);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
    transition: background .16s ease, border-color .16s ease;
}
.ui-sheet__choice-btn:hover { background: var(--ui-surface-3, var(--ui-surface-2)); }
/* State is never carried by colour alone (UI-STANDARD.md §6) — the chosen
   theme also gains weight, so it reads without colour vision. */
.ui-sheet__choice-btn[aria-pressed="true"] {
    border-color: var(--ui-accent);
    background: var(--ui-accent-soft, var(--ui-surface-2));
    font-weight: 800;
}

/*
 * The drawer's own heading: whose account this is.
 *
 * Bigger than the group labels beneath it and not uppercase, because it is a
 * name and not a category — a person's name set in tracked-out capitals reads
 * as a filing label rather than as them.
 */
.ui-sheet__who {
    margin: var(--ui-s-2) 0 var(--ui-s-1);
    padding: 0 10px;
    font-size: 1.0625rem;
    font-weight: 800;
    letter-spacing: -0.01em;
    color: var(--ui-text);
}
.ui-sheet__who[hidden] { display: none; }

/*
 * Every group label in the drawer gets air above it, so SWITCH THEME, TEXT
 * SIZE and ACCOUNT read as three groups rather than one long list with
 * headings in it. The first needs none — the sheet's own padding is already
 * above it.
 */
.ui-sheet__panel .ui-nav__menu-label { margin-top: var(--ui-s-4); }
.ui-sheet__panel > .ui-nav__menu-label:first-of-type,
.ui-sheet__who + .ui-nav__menu-label { margin-top: var(--ui-s-2); }

/* ─── Avatar notification count ──────────────────────────────────────
   Top right of the avatar, capped at 99+. Same number the bell shows on
   desktop; on a phone the bell is gone with the header, so this is the
   only place the count appears. */
.ui-avatar-wrap { position: relative; }

/* Both notification badges are `display: inline-grid`, which outranks the
   browser's own `[hidden] { display: none }`. Without these two rules the
   hidden attribute is decorative, and paintAvatarBadges() setting
   `badge.hidden = true` leaves a red "0" on screen — which is exactly what
   every signed-out visitor saw on the More tab. */
.ui-avatar-badge[hidden],
.ui-nav__menu-count[hidden] { display: none; }

.ui-avatar-badge {
    position: absolute;
    top: -5px;
    left: 24px;
    z-index: 1;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    display: inline-grid;
    place-items: center;
    border-radius: 999px;
    background: #e11d48;
    color: #fff;
    font-size: 0.625rem;
    font-weight: 800;
    line-height: 1;
    letter-spacing: 0;
    box-shadow: 0 0 0 2px var(--ui-surface-0);
    pointer-events: none;
}

/* In the bottom bar the badge hangs off the icon, which is centred in a
   tab a fifth of the screen wide — so it is placed from the centre, not
   from the tab's edge. */
.ui-bottomnav__item { position: relative; }
.ui-bottomnav__item > .ui-avatar-badge { top: 0; left: calc(50% + 6px); }

/* One count, one place. With the number on the avatar, the bell beside it
   was the same red pill twice; the Notifications page is one item down the
   account menu. */
body[data-ui="v2"] .ui-nav__notifications { display: none; }

/* "Your plan" in the account menu and the More sheet. The rows are flex,
   which outranks [hidden], so the guard is explicit. The pill sits on the
   right: Plus in gold, Trial in the quieter accent. */
[data-plan-row][hidden],
.ui-plan-pill[hidden] { display: none !important; }
.ui-plan-pill {
    margin-left: auto;
    flex: none;
    display: inline-flex;
    align-items: center;
    min-height: 22px;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: 0.02em;
    text-transform: none;
    color: var(--cr-accent, #7d5110);
    background: var(--cr-accent-soft, rgba(217, 164, 65, 0.16));
    border: 1px solid var(--cr-accent-line, rgba(217, 164, 65, 0.5));
}
/* The desktop menu is not on the crest's palette: Trial borrows its text
   colour so it reads in both themes. */
.ui-nav__menu .ui-plan-pill:not([data-kind="plus"]) { color: inherit; background: transparent; border-color: currentColor; }
.ui-plan-pill[data-kind="plus"] {
    color: #2a2012;
    background: linear-gradient(160deg, var(--cr-gold-2, #f0cf86), var(--cr-gold, #d9a441));
    border-color: transparent;
}

/* The count beside "Notifications" in the account menu and the More sheet. */
.ui-nav__menu-count {
    margin-left: auto;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    display: inline-grid;
    place-items: center;
    border-radius: 999px;
    background: #e11d48;
    color: #fff;
    font-size: 0.6875rem;
    font-weight: 800;
    line-height: 1;
}

/* Below 900px the header is gone entirely: the bottom bar carries every
   destination and the More sheet the account, so a second bar at the top
   was 64px of chrome doing nothing but repeating the wordmark. */
/* Keyed on data-nav, which renderAppNav sets, rather than on data-ui: a page
   can carry the application nav without being a v2 layout (/profile does), and
   when it does it must navigate like every other page. */
@media (max-width: 899px) {
    body[data-nav="app"] .ui-nav { display: none; }
}

/* Below 900px the bar sits over the last of the page. Reserve its height
   so a footer link or a submit button is never trapped behind it. */
@media (max-width: 899px) {
    body[data-nav="app"] { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
}

/* ─── The bar on a game page ────────────────────────────────────────── */
/* A game page is one screen with two states. `.ui-screen` is the overlay a
   game shows for setup, mode choice and the result; it is down while a match
   is being played. The bar follows it: there in the lobby, where leaving is a
   thing you might want to do, gone under the board, where a tab bar competes
   with the game's own controls and the chat head. portal-components.js sets
   both attributes from the screens the games already toggle. */
/* A game page keeps its own chrome on desktop — a board, or the game's own
   topbar. The shared header has nothing to add there and would be a second
   bar over the board, so on these pages the app nav is the phone bar only. */
body[data-nav-game="1"] .ui-nav { display: none; }

@media (max-width: 899px) {
    /* .ui-screen is a fixed overlay at z-index 200. Without this the bar
       renders behind the very screen it is meant to sit on. */
    body[data-nav-game="1"] .ui-bottomnav { z-index: 240; }
    body[data-nav-game="1"] .ui-sheet { z-index: 260; }

    body[data-nav-game="1"]:not([data-nav-lobby]) .ui-bottomnav,
    body[data-nav-game="1"]:not([data-nav-lobby]) .ui-sheet { display: none; }

    /* The bar overlays the page rather than the document flow here, so the
       height is reserved on the overlay's own scroll box, and only while the
       bar is actually up. The body reservation would otherwise leave 64px of
       dead space under a board that has no bar over it. */
    body[data-nav="app"][data-nav-game="1"] { padding-bottom: 0; }

    /* An overlay lobby reserves the height inside its own scroll box; an
       in-flow one (Bottle Sort) reserves it on the body like any page. */
    body[data-nav-game="1"][data-nav-lobby="overlay"] .ui-screen {
        padding-bottom: calc(64px + var(--ui-s-4) + env(safe-area-inset-bottom, 0px));
    }
    body[data-nav="app"][data-nav-game="1"][data-nav-lobby="flow"] {
        padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px));
    }
}


/* ─── Hero ──────────────────────────────────────────────────────────── */
/* A deliberate ink panel — dark in both themes. No hotlinked photography;
   the texture is generated, so it can never 404 or slow the page down. */

.ui-hero {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    border-radius: var(--ui-r-2xl);
    background: var(--ui-ink);
    color: var(--ui-ink-text);
    padding: var(--ui-s-10) var(--ui-s-6);
}
@media (min-width: 768px) { .ui-hero { padding: var(--ui-s-16) var(--ui-s-12); } }

/* Tone wash + soft grid, both generated */
.ui-hero::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -2;
    background:
        radial-gradient(ellipse 80% 70% at 8% 0%,
            color-mix(in srgb, var(--ui-tone) 55%, transparent) 0%, transparent 62%),
        radial-gradient(ellipse 70% 80% at 100% 100%,
            color-mix(in srgb, var(--ui-accent) 42%, transparent) 0%, transparent 60%);
}
.ui-hero::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background-image:
        linear-gradient(var(--ui-ink-border) 1px, transparent 1px),
        linear-gradient(90deg, var(--ui-ink-border) 1px, transparent 1px);
    background-size: 56px 56px;
    mask-image: radial-gradient(ellipse 70% 90% at 70% 20%, #000 0%, transparent 75%);
    -webkit-mask-image: radial-gradient(ellipse 70% 90% at 70% 20%, #000 0%, transparent 75%);
    opacity: 0.6;
}

.ui-hero__inner { position: relative; max-width: 640px; }
.ui-hero--center .ui-hero__inner { max-width: 720px; margin-inline: auto; text-align: center; }

.ui-hero__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 5px 12px;
    border-radius: var(--ui-r-full);
    background: var(--ui-ink-2);
    border: 1px solid var(--ui-ink-border);
    color: var(--ui-ink-text);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.ui-hero__title {
    margin: var(--ui-s-5) 0 0;
    font-size: clamp(2.125rem, 1.3rem + 3.4vw, 3.75rem);
    line-height: 1.04;
    font-weight: 800;
    letter-spacing: -0.038em;
    color: var(--ui-ink-text);
    text-wrap: balance;
}
.ui-hero__title em {
    font-style: normal;
    color: transparent;
    background: linear-gradient(100deg, var(--ui-tone), color-mix(in srgb, var(--ui-tone) 35%, #fff));
    -webkit-background-clip: text;
    background-clip: text;
}

.ui-hero__text {
    margin: var(--ui-s-5) 0 0;
    font-size: 1.0625rem;
    line-height: 1.65;
    font-weight: 400;
    color: var(--ui-ink-muted);
    max-width: 52ch;
}
.ui-hero--center .ui-hero__text { margin-inline: auto; }

.ui-hero__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ui-s-3);
    margin-top: var(--ui-s-8);
}
.ui-hero--center .ui-hero__actions { justify-content: center; }

/* Buttons sitting on ink need their own contrast rules */
.ui-hero .ui-btn--primary {
    background: var(--ui-tone);
    color: #fff;
    box-shadow: 0 4px 16px color-mix(in srgb, var(--ui-tone) 40%, transparent);
}
.ui-hero .ui-btn--primary:hover:not(:disabled) {
    background: color-mix(in srgb, var(--ui-tone) 84%, #fff);
}
.ui-hero .ui-btn--secondary {
    background: var(--ui-ink-2);
    border-color: var(--ui-ink-border);
    color: var(--ui-ink-text);
}
.ui-hero .ui-btn--secondary:hover:not(:disabled) {
    background: color-mix(in srgb, #fff 14%, transparent);
    border-color: color-mix(in srgb, #fff 26%, transparent);
}

/* Stat strip under the hero copy */
.ui-hero__stats {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ui-s-8);
    margin-top: var(--ui-s-10);
    padding-top: var(--ui-s-6);
    border-top: 1px solid var(--ui-ink-border);
}
.ui-hero--center .ui-hero__stats { justify-content: center; }
.ui-hero__stat-value {
    display: block;
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--ui-ink-text);
    font-variant-numeric: tabular-nums;
}
.ui-hero__stat-label {
    display: block;
    margin-top: 2px;
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--ui-ink-muted);
}


/* ─── Portal switcher ───────────────────────────────────────────────── */

.ui-switcher {
    display: grid;
    gap: var(--ui-s-3);
    grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 720px) { .ui-switcher { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.ui-switch-card {
    display: flex;
    align-items: center;
    gap: var(--ui-s-3);
    padding: var(--ui-s-4);
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    text-decoration: none;
    transition: border-color var(--ui-fast) var(--ui-ease),
                transform var(--ui-fast) var(--ui-ease),
                box-shadow var(--ui-fast) var(--ui-ease);
}
.ui-switch-card:hover {
    border-color: var(--ui-tone-line);
    transform: translateY(-2px);
    box-shadow: var(--ui-shadow-md);
}
.ui-switch-card[aria-current="page"] {
    border-color: var(--ui-tone);
    background: var(--ui-tone-soft);
}

.ui-switch-card__icon {
    width: 40px;
    height: 40px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: 11px;
    background: var(--ui-tone-soft);
    border: 1px solid var(--ui-tone-line);
    font-size: 1.25rem;
}
.ui-switch-card__body { min-width: 0; }
.ui-switch-card__name {
    display: block;
    font-size: 0.9375rem;
    font-weight: 700;
    color: var(--ui-text);
    letter-spacing: -0.015em;
}
.ui-switch-card__desc {
    display: block;
    margin-top: 1px;
    font-size: 0.8125rem;
    color: var(--ui-text-muted);
}
.ui-switch-card__here {
    margin-left: auto;
    flex: none;
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ui-tone-text);
}


/* ─── Section headers ───────────────────────────────────────────────── */

.ui-section-head {
    display: flex;
    /* Top-aligned. flex-end pinned the subtitle's baseline to the bottom of
       the title, so a one-line description floated level with the heading's
       descenders instead of sitting under it where it belongs. */
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--ui-s-4);
    margin-bottom: var(--ui-s-5);
}
.ui-section-head__title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 700;
    letter-spacing: -0.022em;
    color: var(--ui-text);
}
.ui-section-head__sub {
    margin: 3px 0 0;
    font-size: 0.875rem;
    color: var(--ui-text-muted);
}
.ui-section-head__action { flex: none; }


/* ─── Content cards (challenges) ────────────────────────────────────── */

.ui-tile {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    box-shadow: var(--ui-shadow-sm);
    text-decoration: none;
    transition: border-color var(--ui-base) var(--ui-ease),
                transform var(--ui-base) var(--ui-ease),
                box-shadow var(--ui-base) var(--ui-ease);
}
.ui-tile:hover {
    border-color: var(--ui-tone-line);
    transform: translateY(-3px);
    box-shadow: var(--ui-shadow-lg);
}

.ui-tile__media {
    position: relative;
    display: block;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    background: var(--ui-surface-inset);
}
.ui-tile__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 600ms var(--ui-ease);
}
.ui-tile:hover .ui-tile__media img { transform: scale(1.04); }

/* Fallback when a challenge has no cover image */
.ui-tile__media--empty {
    display: grid;
    place-items: center;
    font-size: 2rem;
    background:
        radial-gradient(circle at 30% 25%, var(--ui-tone-soft) 0%, transparent 60%),
        var(--ui-surface-inset);
}

.ui-tile__tag {
    position: absolute;
    top: 10px;
    left: 10px;
    padding: 4px 10px;
    border-radius: var(--ui-r-full);
    background: color-mix(in srgb, var(--ui-surface-1) 88%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--ui-border);
    color: var(--ui-tone-text);
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
}

.ui-tile__body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    padding: var(--ui-s-4);
    gap: var(--ui-s-2);
}
.ui-tile__title {
    margin: 0;
    font-size: 1rem;
    font-weight: 700;
    line-height: 1.35;
    letter-spacing: -0.015em;
    color: var(--ui-text);
}
.ui-tile__text {
    margin: 0;
    font-size: 0.8125rem;
    line-height: 1.6;
    color: var(--ui-text-muted);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.ui-tile__foot {
    display: flex;
    gap: var(--ui-s-2);
    margin-top: auto;
    padding-top: var(--ui-s-3);
}
.ui-tile__foot > .ui-btn { flex: 1 1 auto; }


/* ─── Poll cards ────────────────────────────────────────────────────── */

.ui-poll {
    display: flex;
    flex-direction: column;
    gap: var(--ui-s-3);
    padding: var(--ui-s-5);
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    box-shadow: var(--ui-shadow-sm);
}
.ui-poll__media {
    margin: calc(var(--ui-s-5) * -1) calc(var(--ui-s-5) * -1) 0;
    aspect-ratio: 16 / 7;
    overflow: hidden;
    background: var(--ui-surface-inset);
}
.ui-poll__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ui-poll__title {
    margin: 0;
    font-size: 1.0625rem;
    font-weight: 700;
    line-height: 1.35;
    letter-spacing: -0.015em;
    color: var(--ui-text);
}
.ui-poll__text {
    margin: 0;
    font-size: 0.8125rem;
    line-height: 1.6;
    color: var(--ui-text-muted);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.ui-poll__summary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ui-s-2);
    margin-top: auto;
    padding-top: var(--ui-s-2);
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--ui-text-muted);
}
.ui-poll__action {
    padding-top: var(--ui-s-1);
}
.ui-poll__choices { display: flex; flex-direction: column; gap: var(--ui-s-2); }

.ui-poll-modal__hint {
    margin: 0;
    padding: var(--ui-s-3) var(--ui-s-4);
    border: 1px solid var(--ui-tone-line);
    border-radius: var(--ui-r-md);
    background: var(--ui-tone-soft);
    color: var(--ui-text-secondary);
    font-size: 0.8125rem;
    line-height: 1.55;
}
.ui-poll-modal__badges { padding-right: var(--ui-s-8); }
.ui-poll-modal__loading { padding-block: var(--ui-s-6); }
.ui-poll-modal__foot {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--ui-s-2) var(--ui-s-4);
    padding-top: var(--ui-s-3);
    border-top: 1px solid var(--ui-border);
    color: var(--ui-text-muted);
    font-size: 0.75rem;
    font-weight: 600;
}

@media (max-width: 639px) {
    .ui-poll-modal {
        max-height: calc(100dvh - var(--ui-s-4));
        padding: var(--ui-s-5);
    }
}

.ui-choice {
    position: relative;
    overflow: hidden;
    width: 100%;
    padding: 11px 14px;
    text-align: left;
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    cursor: pointer;
    font: inherit;
    transition: border-color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-choice:hover:not(:disabled) { border-color: var(--ui-tone-line); }
.ui-choice:disabled { cursor: default; }
.ui-choice.is-picked { border-color: var(--ui-tone); background: var(--ui-tone-soft); }

/* The result bar. Width animates from 0 via the data-width attribute. */
.ui-choice__bar {
    position: absolute;
    inset: 0 auto 0 0;
    width: 0;
    background: var(--ui-tone-soft);
    transition: width 700ms var(--ui-ease);
}
.ui-choice__row {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-3);
}
.ui-choice__label { font-size: 0.875rem; font-weight: 600; color: var(--ui-text); }
.ui-choice__pct {
    font-size: 0.8125rem;
    font-weight: 700;
    color: var(--ui-tone-text);
    font-variant-numeric: tabular-nums;
    flex: none;
}


/* ─── Ink panel (spotlight / promo) ─────────────────────────────────── */

.ui-ink-panel {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    padding: var(--ui-s-6);
    border-radius: var(--ui-r-xl);
    background: var(--ui-ink);
    border: 1px solid var(--ui-ink-border);
    color: var(--ui-ink-muted);
}
.ui-ink-panel::before {
    content: "";
    position: absolute;
    inset: -40% -30% auto auto;
    width: 260px;
    height: 260px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--ui-tone) 40%, transparent);
    filter: blur(60px);
    z-index: -1;
}
.ui-ink-panel h2, .ui-ink-panel h3, .ui-ink-panel .ui-card__title { color: var(--ui-ink-text); }
.ui-ink-panel .ui-eyebrow { color: var(--ui-tone-text); }
.ui-ink-panel .ui-card__head { border-bottom-color: var(--ui-ink-border); }


/* ─── Footer ────────────────────────────────────────────────────────── */

.ui-footer {
    margin-top: var(--ui-s-16);
    border-top: 1px solid var(--ui-border);
    background: var(--ui-surface-1);
}
.ui-footer__inner {
    max-width: 1200px;
    margin-inline: auto;
    padding: var(--ui-s-8) var(--ui-s-4);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ui-s-4);
}
@media (min-width: 640px)  { .ui-footer__inner { padding-inline: var(--ui-s-6); } }
@media (min-width: 1024px) { .ui-footer__inner { padding-inline: var(--ui-s-8); } }

.ui-footer__links { display: flex; flex-wrap: wrap; gap: var(--ui-s-4); margin-left: auto; }
.ui-footer__link {
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--ui-text-muted);
    text-decoration: none;
    transition: color var(--ui-fast) var(--ui-ease);
}
.ui-footer__link:hover { color: var(--ui-text); }
.ui-footer__note { font-size: 0.8125rem; color: var(--ui-text-faint); width: 100%; }

/* The install entry is a BUTTON, because on Android it fires Chrome's prompt
   rather than going anywhere — a link that navigates nowhere is a lie to a
   screen reader and to anyone who middle-clicks it. It is styled as one of the
   footer links because that is what it is doing in the row, so the element is
   reset back to text here rather than the row learning about buttons. */
button.ui-footer__link {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    font-family: inherit;
    cursor: pointer;
}
.ui-footer__install {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
}
/* iOS Safari has no prompt, so the steps are spelled out under the row. */
.ui-footer__install-note {
    font-size: 0.8125rem;
    line-height: 1.6;
    color: var(--ui-text-faint);
    width: 100%;
    margin: 0;
}

/* ── The footer is a desktop thing, and a landing-page thing ───────────
   Below 900px the bottom nav is on screen -- five tabs and More, fixed to
   the edge of the screen, carrying every destination the footer lists. A
   second stack of the same links directly above it is not navigation, it is
   a wall the reader has to scroll past to prove they reached the end, and it
   sits under the one bar that is always there.

   It stays on the landing pages (home, homepage), where there is no app
   around it: somebody who has not signed in arrives at the bottom of a page
   wanting Privacy, Terms or Support, and nothing else on the screen offers
   them. Those pages opt in with `data-footer="always"` on <body> -- an
   attribute, so a new landing page asks for it rather than inheriting a
   URL list nobody remembers to update.

   The theme switch lives in this footer, so it follows it: reachable from
   the landing pages on a phone, and from the footer on any desktop width.
   The mobile app surface has its own controls in the More sheet. */
@media (max-width: 899.98px) {
    .ui-footer { display: none; }
    body[data-footer="always"] .ui-footer { display: block; }
}


/* ─── Junior tuning ─────────────────────────────────────────────────── */
/* Junior earns its personality from radius, scale and touch targets —
   not from a different layout or a pile of one-off styles. */

body[data-portal="junior"] {
    --ui-r-md: 14px;
    --ui-r-lg: 20px;
    --ui-r-xl: 26px;
    --ui-r-2xl: 32px;
}
body[data-portal="junior"] .ui-btn { padding: 12px 22px; font-weight: 700; }
body[data-portal="junior"] .ui-btn--sm { padding: 9px 15px; }
body[data-portal="junior"] .ui-tile__title,
body[data-portal="junior"] .ui-section-head__title { font-weight: 800; }
body[data-portal="junior"] .ui-nav__link { font-weight: 700; }


/* ─── Profile widget (v2 skin) ──────────────────────────────────────── */
/* portal-components.js renders the widget with these class hooks on v2
   pages so its colours come from tokens and follow the theme toggle,
   instead of being baked in as Tailwind classes at render time. */

body[data-ui="v2"] .ui-chip-shell {
    background: var(--ui-surface-1);
    border-color: var(--ui-border) !important;
    color: var(--ui-text);
    border-radius: var(--ui-r-full);
}
body[data-ui="v2"] .ui-chip-shell a { color: inherit; text-decoration: none; }

body[data-ui="v2"] .ui-chip-muted { color: var(--ui-text-muted); }

body[data-ui="v2"] .ui-chip-btn {
    color: var(--ui-text-muted);
    border-radius: var(--ui-r-full);
    transition: background var(--ui-fast) var(--ui-ease), color var(--ui-fast) var(--ui-ease);
}
body[data-ui="v2"] .ui-chip-btn:hover {
    background: var(--ui-surface-3);
    color: var(--ui-text);
}

/* Tame the widget's inherited micro-typography */
body[data-ui="v2"] .ui-chip-shell p { font-size: 0.75rem; font-weight: 600; letter-spacing: 0; }
body[data-ui="v2"] .ui-chip-shell .ui-chip-muted { font-size: 0.625rem; font-weight: 600; }


/* ══════════════════════════════════════════════════════════════════════
   16. SUBPAGE SHELL
   ----------------------------------------------------------------------
   Pieces the portal pages didn't need: filter tabs, rank rows, generated
   game artwork, notification rows and a data table.
   ══════════════════════════════════════════════════════════════════════ */

/* ─── Page header ───────────────────────────────────────────────────── */

/*
 * A title sits CLOSE to what it titles.
 *
 * 32px below the header, on top of the tab row's own leading, put most of a
 * phone's first screen between the word "Quests" and the first quest --
 * enough air that the title read as its own section rather than as a label
 * for the row under it. Proximity is the only thing saying the two belong
 * together, so the gap under the head is now smaller than the gap under the
 * tabs, which is what makes the grouping read at a glance.
 *
 * 16px is the floor for a heading against body-sized text, not a guess:
 * below that the descenders of the title start crowding the row beneath.
 * Pages whose head is followed by prose keep more, via .ui-page-head__sub's
 * own spacing.
 */
.ui-page-head { margin-bottom: var(--ui-s-4); }
/*
 * ONE header for Quests, Tools and Games: an h1 in .ui-page-head, and
 * nothing else. No subtitle -- on all three the row under the title (tabs,
 * or the library itself) says what the page holds, and a line of copy above
 * it only pushed the content down a phone screen.
 *
 * The floor was 1.75rem/28px, which on a 360px screen took the word "Quests"
 * to roughly a fifth of the height above the fold before a single quest was
 * visible. A page title is a label, not a banner: it says where you are and
 * then gets out of the way. 24px is still unmistakably the largest thing on
 * the screen; the desktop end is unchanged in kind, just less shouty.
 */
.ui-page-head__title {
    margin: 0;
    font-size: clamp(1.5rem, 1.15rem + 1.4vw, 2.25rem);
    line-height: 1.1;
    font-weight: 800;
    letter-spacing: -0.032em;
    color: var(--ui-text);
}
.ui-page-head__sub {
    margin: var(--ui-s-3) 0 0;
    font-size: 1rem;
    line-height: 1.6;
    color: var(--ui-text-secondary);
    max-width: 58ch;
}


/* ─── Filter tabs ───────────────────────────────────────────────────── */
/* Horizontally scrollable on mobile without a visible scrollbar, but still
   keyboard reachable — the old version hid overflow entirely. */

.ui-tabs {
    display: flex;
    gap: var(--ui-s-2);
    overflow-x: auto;
    padding-bottom: var(--ui-s-3);
    margin-bottom: var(--ui-s-5);
    border-bottom: 1px solid var(--ui-border);
    scrollbar-width: none;
}
.ui-tabs::-webkit-scrollbar { display: none; }

.ui-tab {
    flex: none;
    padding: 8px 16px;
    border-radius: var(--ui-r-full);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
    color: var(--ui-text-muted);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease);
}
.ui-tab:hover { color: var(--ui-text); border-color: var(--ui-border-strong); }
.ui-tab[aria-selected="true"] {
    background: var(--ui-tone-soft);
    border-color: var(--ui-tone-line);
    color: var(--ui-tone-text);
}


/* ─── Section tabs, underline style ─────────────────────────────────── */
/*
 * The group page rail shipped drawing TWO horizontal lines: a border-bottom
 * on the container, and a 2px border-bottom on every button. Worse, the
 * per-button line was drawn under the inactive tabs as well as the active
 * one, so the underline — whose entire job is to say WHICH TAB YOU ARE ON —
 * said nothing, and the container rule sat a pixel below it looking like a
 * rendering fault.
 *
 * One line, under the selected tab only. No rule under the row: the content
 * below already starts with a card, and a second boundary a few pixels away
 * from a real one is decoration pretending to be structure.
 */
.ui-tabset {
    display: flex;
    gap: var(--ui-s-1);
    overflow-x: auto;
    scrollbar-width: none;
    margin-bottom: var(--ui-s-5);
}
.ui-tabset::-webkit-scrollbar { display: none; }

.ui-tabset__tab {
    flex: none;
    appearance: none;
    background: none;
    border: 0;
    /* Transparent, not absent, so selecting a tab does not shift the row. */
    border-bottom: 2px solid transparent;
    padding: 10px 14px;
    font: inherit;
    font-size: 0.75rem;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--ui-text-muted);
    cursor: pointer;
    transition: color var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease);
}
.ui-tabset__tab:hover { color: var(--ui-text); }
.ui-tabset__tab[aria-selected="true"] {
    color: var(--ui-tone-text);
    border-bottom-color: var(--ui-tone-text);
}
@media (max-width: 640px) {
    .ui-tabset__tab { padding-inline: 10px; letter-spacing: 0.04em; }
}


/* ─── Page tabs ─────────────────────────────────────────────────────── */
/*
 * THE tab row: icon, label, count.
 *
 * The quests page grew this shape by hand in Tailwind -- an underline rail
 * where each tab carries an icon, a name and a pill with how many things are
 * behind it -- and it is the best tab row in the product: the count is the
 * reason you pick a tab, and reading "Saved 0" before you tap saves the tap.
 * Tools had the pill-style .ui-tabs instead, so two pages that do the same
 * job (a library of things, filtered by your relationship to them: in
 * progress, find, saved, done) looked like two different products.
 *
 * It is one component now, here, rather than a Tailwind string copied into a
 * second page -- where it would drift the first time either page was touched.
 *
 * SELECTION IS `aria-selected`, NOT A CLASS SWAP. The old quests version
 * rewrote each button's entire className on every switch, which is both how
 * you lose an unrelated class by accident and a second source of truth about
 * which tab is on. The attribute the screen reader reads is the attribute
 * that draws it.
 */
.ui-tabbar {
    display: flex;
    gap: var(--ui-s-2);
    overflow-x: auto;
    scrollbar-width: none;
    /* The rule belongs to the ROW, not to each tab -- an inactive tab must
       not draw its own line, or the underline stops meaning "you are here". */
    border-bottom: 1px solid var(--ui-border);
    margin-bottom: var(--ui-s-5);
}
.ui-tabbar::-webkit-scrollbar { display: none; }
@media (min-width: 640px) { .ui-tabbar { gap: var(--ui-s-6); } }

.ui-tabbar__tab {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: var(--ui-s-2);
    appearance: none;
    background: none;
    border: 0;
    /* Transparent, not absent: selecting a tab must not shift the row. */
    border-bottom: 2px solid transparent;
    /* 44px of press target, which is the floor on a phone, not a preference. */
    padding: 12px 8px;
    margin-bottom: -1px;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 500;
    white-space: nowrap;
    color: var(--ui-text-muted);
    cursor: pointer;
    transition: color var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease);
}
.ui-tabbar__tab:hover { color: var(--ui-text); border-bottom-color: var(--ui-border-strong); }
.ui-tabbar__tab:focus-visible { outline: 2px solid var(--ui-tone); outline-offset: -2px; }
.ui-tabbar__tab[aria-selected="true"] {
    font-weight: 700;
    color: var(--ui-tone-text);
    border-bottom-color: var(--ui-tone-text);
}

.ui-tabbar__icon { width: 16px; height: 16px; flex: none; }

/* The count. Quiet until its tab is selected -- four loud pills in a row is
   four things competing to be read first. */
.ui-tabbar__count {
    flex: none;
    min-width: 20px;
    padding: 2px 8px;
    border-radius: var(--ui-r-full);
    background: var(--ui-surface-3);
    color: var(--ui-text-muted);
    font-size: 0.6875rem;
    font-weight: 700;
    line-height: 1.4;
    text-align: center;
}
.ui-tabbar__tab[aria-selected="true"] .ui-tabbar__count {
    background: var(--ui-tone-soft);
    color: var(--ui-tone-text);
}

/* ─── Filter bar ────────────────────────────────────────────────────── */
/*
 * Filter controls stay on ONE line at every width, however many there are.
 *
 * This was a wrapping flex row: the search field carried `min-width: 220px`
 * and each `.ui-select--auto` carries `min-width: 150px`. That is over 500px
 * of hard minimum, so on any phone the selects dropped to a second and third
 * row — a filter bar taller than the first result it was filtering.
 *
 * Flex rather than a fixed grid template because the number of controls
 * varies: the Discover bar hides its space filter for anyone in fewer than
 * two spaces, and a `grid-template-columns` with a counted number of tracks
 * would have to be rewritten every time a filter is added or hidden.
 */
.ui-filter-bar {
    display: flex;
    flex-wrap: nowrap;
    gap: var(--ui-s-2);
    align-items: center;
}
/* Every child must be allowed below its content width, or one long option
   label in a <select> widens the bar and then the page. */
.ui-filter-bar > * { min-width: 0; }

/* The search field is the only control that WANTS space: it holds a phrase
   of unknown length, and every pixel is legible. It takes the remainder. */
.ui-filter-bar > input { flex: 1 1 0; }

/*
 * `width: auto` is doing real work here, and its absence is what produced a
 * bar with two huge selects and a search box crushed to a sliver.
 *
 * `.ui-select` sets `width: 100%` — correct in a stacked form, wrong in a
 * row. With `flex-basis: auto` that 100% BECOMES the basis, so each select
 * asked for the full width of the bar. Two of them over-subscribed it, they
 * shrank against each other to roughly half each, and the input — basis 0 —
 * was left the crumbs. The old `.ui-select--auto` existed to cancel exactly
 * this; dropping it without replacing it is what broke desktop.
 *
 * So: size to content, do not grow. A dropdown showing one short value has
 * nothing to do with extra width.
 */
.ui-filter-bar > select {
    flex: 0 0 auto;
    width: auto;
    /* Options carry group names, and `width: auto` on a select measures the
       LONGEST option — one verbosely-named group would otherwise set the
       width of a control whose visible value is "All groups". */
    max-width: 200px;
    text-overflow: ellipsis;
}

@media (max-width: 640px) {
    /* Now the row is genuinely tight, so the selects share what is left by
       proportion rather than sizing to content. Search keeps the larger
       share: a select still reads at 80px because it shows one value, while
       a search box that narrow is a puzzle. */
    .ui-filter-bar > input  { flex: 1.5 1 0; }
    .ui-filter-bar > select { flex: 1 1 0; max-width: none; }
    .ui-filter-bar > select,
    .ui-filter-bar > input {
        font-size: 0.8125rem;
        padding-left: 10px;
    }
    .ui-filter-bar > select { padding-right: 24px; }
    .ui-filter-bar > input  { padding-right: 10px; }
}


/* ─── Rank rows (leaderboards) ──────────────────────────────────────── */

.ui-rank {
    display: flex;
    align-items: center;
    gap: var(--ui-s-3);
    padding: 9px 11px;
    border-radius: var(--ui-r-md);
    border: 1px solid transparent;
    text-decoration: none;
    transition: background var(--ui-fast) var(--ui-ease);
}
.ui-rank:hover { background: var(--ui-surface-3); }

.ui-rank__pos {
    width: 24px;
    height: 24px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: var(--ui-r-full);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    color: var(--ui-text-muted);
    font-size: 0.6875rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.ui-rank__body { flex: 1 1 auto; min-width: 0; }
.ui-rank__name {
    display: block;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ui-rank__meta {
    display: block;
    margin-top: 1px;
    font-size: 0.6875rem;
    color: var(--ui-text-muted);
    font-variant-numeric: tabular-nums;
}

/* Podium: gold / silver / bronze, readable in both themes */
.ui-rank--1 { background: var(--ui-warning-soft); border-color: color-mix(in srgb, var(--ui-warning) 30%, transparent); }
.ui-rank--1 .ui-rank__pos { background: var(--ui-warning); border-color: var(--ui-warning); color: #422006; }
.ui-rank--2 .ui-rank__pos { background: var(--ui-text-faint); border-color: var(--ui-text-faint); color: var(--ui-surface-1); }
.ui-rank--3 .ui-rank__pos { background: #b45309; border-color: #b45309; color: #fff; }

/* "you" highlight */
.ui-rank--self { background: var(--ui-tone-soft); border-color: var(--ui-tone-line); }
.ui-rank--self .ui-rank__name { color: var(--ui-tone-text); }


/* ─── Generated game artwork ────────────────────────────────────────── */
/* The old page hand-wrote 28 divs per Connect-4 thumbnail. These build the
   same idea from a few elements and inherit the card's tone. */

.ui-art {
    position: relative;
    display: grid;
    place-items: center;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-radius: var(--ui-r-md);
    background:
        radial-gradient(circle at 30% 20%, var(--ui-tone-soft) 0%, transparent 65%),
        var(--ui-surface-inset);
    border: 1px solid var(--ui-border);
}

.ui-art__grid {
    display: grid;
    gap: 5px;
    padding: 10px;
    border-radius: 10px;
    background: color-mix(in srgb, var(--ui-tone) 22%, var(--ui-surface-inset));
}
.ui-art__cell {
    width: 15px;
    height: 15px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--ui-surface-inset) 82%, #000);
}
.ui-art__cell--a { background: #ef4444; }
.ui-art__cell--b { background: #facc15; }

.ui-art__tiles { display: flex; gap: 7px; }
.ui-art__tile {
    width: 34px;
    height: 42px;
    display: grid;
    place-items: center;
    border-radius: 9px;
    background: var(--ui-surface-2);
    border: 1.5px solid var(--ui-tone-line);
    color: var(--ui-tone-text);
    font-size: 1.125rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.ui-art__tile--lit {
    border-color: var(--ui-tone);
    box-shadow: 0 0 0 3px var(--ui-tone-soft);
}

.ui-art__word {
    font-family: ui-monospace, 'Space Mono', monospace;
    font-size: 1rem;
    font-weight: 700;
    line-height: 1.5;
    letter-spacing: 0.35em;
    text-align: center;
    color: var(--ui-text-faint);
}
.ui-art__word mark {
    background: var(--ui-tone-soft);
    color: var(--ui-tone-text);
    border-radius: 4px;
    padding: 0 3px;
}

.ui-art__badge {
    position: absolute;
    top: 9px;
    right: 9px;
    padding: 3px 9px;
    border-radius: var(--ui-r-full);
    background: color-mix(in srgb, var(--ui-surface-1) 88%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--ui-border);
    color: var(--ui-tone-text);
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}


/* ─── Notification rows ─────────────────────────────────────────────── */

.ui-note {
    display: flex;
    align-items: flex-start;
    gap: var(--ui-s-3);
    padding: var(--ui-s-4);
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    text-decoration: none;
    transition: border-color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-note:hover { border-color: var(--ui-tone-line); }

/* Unread carries a tone bar rather than a whole coloured background —
   a full tint makes a long list unreadable. */
.ui-note--unread { border-left: 3px solid var(--ui-tone); }


.ui-note__icon {
    width: 34px;
    height: 34px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: var(--ui-r-md);
    background: var(--ui-tone-soft);
    border: 1px solid var(--ui-tone-line);
    font-size: 0.9375rem;
}
.ui-note__body { flex: 1 1 auto; min-width: 0; }
.ui-note__title { margin: 0; font-size: 0.875rem; font-weight: 600; color: var(--ui-text); }
.ui-note__text  { margin: 3px 0 0; font-size: 0.8125rem; line-height: 1.55; color: var(--ui-text-muted); }
.ui-note__time  { flex: none; font-size: 0.6875rem; color: var(--ui-text-faint); white-space: nowrap; }

/* The whole row is the primary action.
   A 34px "Open" beside a 34px "Mark read" on a phone is two chances to press
   the wrong one, so the title's link is stretched over the entire card and
   everything else is a quiet control sitting above it. */
.ui-note { position: relative; }
.ui-note__link {
    color: inherit;
    text-decoration: none;
    overflow-wrap: anywhere;
}
.ui-note__link::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
}
.ui-note__link:focus-visible { outline: none; }
.ui-note__link:focus-visible::after {
    outline: 2px solid var(--ui-tone);
    outline-offset: 2px;
}
.ui-note:hover .ui-note__link { text-decoration: underline; }

.ui-note__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ui-s-2);
    margin-top: var(--ui-s-3);
    /* Above the stretched link, or none of these could be pressed. */
    position: relative;
    z-index: 1;
}

/* Category is carried by the unread bar's tone as well as by which section the
   row sits in — state is never colour alone, but colour reinforces it. */
.ui-note--action   { --ui-tone: var(--ui-danger, #e11d48); }
.ui-note--applause { --ui-tone: var(--ui-success, #0f9d76); }

/* ─── Notifications page sections ───────────────────────────────────── */

.ui-notes-section { display: flex; flex-direction: column; gap: var(--ui-s-3); }

/* `.ui-badge` is `display: inline-flex`, which outranks the browser's own
   `[hidden] { display: none }` — the same trap that left a red "0" on the More
   tab for every signed-out visitor. A zero count must vanish, not render. */
.ui-badge[hidden] { display: none; }
.ui-notes-section__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--ui-s-3);
}
.ui-notes-section__title {
    margin: 0;
    font-size: 0.875rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ui-text-muted);
}
.ui-notes-section__list { display: flex; flex-direction: column; gap: var(--ui-s-2); }
.ui-notes-section__list > * { min-width: 0; }



/* ─── Data table ────────────────────────────────────────────────────── */

.ui-table-wrap {
    overflow-x: auto;
    border-radius: var(--ui-r-lg);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-1);
}
.ui-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.ui-table th {
    position: sticky;
    top: 0;
    text-align: left;
    padding: 11px 14px;
    background: var(--ui-surface-2);
    border-bottom: 1px solid var(--ui-border);
    color: var(--ui-text-muted);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
}
.ui-table td {
    padding: 11px 14px;
    border-bottom: 1px solid var(--ui-border);
    color: var(--ui-text-secondary);
}
.ui-table tr:last-child td { border-bottom: 0; }
.ui-table tbody tr:hover td { background: var(--ui-surface-3); }
.ui-table__num { text-align: right; font-variant-numeric: tabular-nums; }


/* ─── Checkbox / radio row ──────────────────────────────────────────── */

.ui-check {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--ui-s-2);
    padding: 10px 14px;
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text-secondary);
    cursor: pointer;
    transition: border-color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-check:hover { border-color: var(--ui-tone-line); }
.ui-check:has(input:checked) {
    background: var(--ui-tone-soft);
    border-color: var(--ui-tone-line);
    color: var(--ui-text);
}
.ui-check:has(input:disabled) { opacity: 0.5; cursor: not-allowed; }

.ui-check input[type="checkbox"],
.ui-check input[type="radio"] {
    width: 16px;
    height: 16px;
    flex: none;
    accent-color: var(--ui-tone);
    cursor: inherit;
}

/* The duel picker's spinner sits inside the modal icon slot */
.ui-modal__icon .ui-spinner { margin: 0 auto; }


/* ─── Stat tiles ────────────────────────────────────────────────────── */

.ui-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--ui-s-2); }
.ui-stat {
    padding: var(--ui-s-3) var(--ui-s-4);
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
}
.ui-stat__label {
    display: block;
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ui-text-muted);
}
.ui-stat__value {
    display: block;
    margin-top: 3px;
    font-size: 1.375rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--ui-tone-text);
    font-variant-numeric: tabular-nums;
}


/* ─── Toast ─────────────────────────────────────────────────────────── */

.ui-toast {
    position: fixed;
    right: var(--ui-s-6);
    bottom: var(--ui-s-6);
    z-index: 400;
    max-width: min(340px, calc(100vw - var(--ui-s-8)));
    padding: 12px 16px;
    border-radius: var(--ui-r-md);
    background: var(--ui-ink);
    border: 1px solid var(--ui-ink-border);
    color: var(--ui-ink-text);
    font-size: 0.8125rem;
    font-weight: 600;
    box-shadow: var(--ui-shadow-lg);
    /* No entry animation here.
       This rule used to run `ui-toast-in`, which animates opacity 0 -> 1. A
       CSS animation overrides declared values while it runs, so on EVERY page
       load the toast animated itself into view showing its placeholder text
       ("Saved.") before settling back to the opacity:0 declared further down.
       Four pages did this. Visibility is owned solely by `.is-visible`. */
}
.ui-toast.hidden { display: none; }
@keyframes ui-toast-in {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}


/* ─── Follow-up note (revealed after voting) ────────────────────────── */

.ui-followup {
    margin-top: var(--ui-s-4);
    padding: var(--ui-s-4);
    border-radius: var(--ui-r-md);
    background: var(--ui-tone-soft);
    border: 1px solid var(--ui-tone-line);
    font-size: 0.8125rem;
    line-height: 1.6;
    color: var(--ui-text-secondary);
}
.ui-followup.hidden { display: none; }
.ui-followup > :first-child { margin-top: 0; }
.ui-followup > :last-child { margin-bottom: 0; }


/* ─── Bullet guide list ─────────────────────────────────────────────── */

.ui-guide { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--ui-s-3); }
.ui-guide > li {
    position: relative;
    padding-left: 18px;
    font-size: 0.8125rem;
    line-height: 1.6;
    color: var(--ui-text-secondary);
}
.ui-guide > li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 8px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--ui-tone);
}


/* ══════════════════════════════════════════════════════════════════════
   17. CHALLENGES
   ══════════════════════════════════════════════════════════════════════ */

/* Views are swapped by showView(); .hidden must win over any display rule. */
.view { display: block; }

.ui-select--auto { width: auto; min-width: 150px; }

.ui-row--end { justify-content: flex-end; }

.ui-toolbar {
    margin-top: var(--ui-s-8);
    padding-top: var(--ui-s-6);
    border-top: 1px solid var(--ui-border);
}

/* ─── Preview ───────────────────────────────────────────────────────── */

.ui-preview-hero {
    display: grid;
    place-items: center;
    aspect-ratio: 21 / 6;
    min-height: 160px;
    overflow: hidden;
    font-size: 3.5rem;
    background:
        radial-gradient(ellipse at 30% 20%, var(--ui-tone-soft) 0%, transparent 60%),
        var(--ui-surface-inset);
    border-bottom: 1px solid var(--ui-border);
}
.ui-preview-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }

.ui-preview-body { padding: var(--ui-s-6); }
@media (min-width: 640px) { .ui-preview-body { padding: var(--ui-s-8); } }

/* Rich text coming out of Quill — style the tags, not per-element classes */
.ui-prose { font-size: 0.9375rem; line-height: 1.7; color: var(--ui-text-secondary); }
.ui-prose > :first-child { margin-top: 0; }
.ui-prose > :last-child { margin-bottom: 0; }
.ui-prose p { margin: 0 0 0.75em; }
.ui-prose ul, .ui-prose ol { margin: 0 0 0.75em; padding-left: 1.25em; }
.ui-prose li { margin-bottom: 0.3em; }
.ui-prose strong, .ui-prose b { color: var(--ui-text); font-weight: 600; }
.ui-prose a { color: var(--ui-tone-text); text-decoration: underline; text-underline-offset: 2px; }

/*
 * Bulleted lists get ticks, not dots.
 *
 * A quest description is where an author now writes what someone will get out
 * of taking part — the outcome list used to be a separate field with its own
 * ✓ rendering, and losing the field should not mean losing the look. A dot
 * says "here is another item"; a tick says "here is something you will have
 * done", which is what these lists are actually for.
 *
 * Numbered lists keep their numbers: an ordered list is a sequence, and a
 * column of identical ticks would destroy the one thing it encodes.
 *
 * `list-style: none` removes the marker box, so the tick is drawn as an
 * inline-block of fixed width pulled into the padding the list already has.
 * That keeps wrapped lines aligned under the text rather than under the tick.
 */
.ui-prose ul { list-style: none; padding-left: 1.5em; }
.ui-prose ul > li { position: relative; }
.ui-prose ul > li::before {
    content: "\2713";                 /* ✓ */
    position: absolute;
    left: -1.35em;
    top: 0;
    width: 1em;
    color: var(--ui-success);
    font-weight: 700;
    line-height: inherit;
}
/* A nested list is a sub-point, not a second set of outcomes. */
.ui-prose ul ul > li::before { content: "\2013"; color: var(--ui-text-faint); }

/*
 * Numbered lists keep their numbers — an ordered list encodes a sequence and
 * a column of identical ticks would destroy it — but the number is set to
 * match the tick beside it: a counter drawn in the same reserved gutter, in
 * the same soft-tinted well, so a "steps" list and an "outcomes" list read as
 * two members of one family rather than two unrelated widgets.
 */
.ui-prose ol { list-style: none; padding-left: 1.9em; counter-reset: ui-prose-step; }
.ui-prose ol > li { position: relative; counter-increment: ui-prose-step; }
/*
 * Every length in this rule is in the pseudo-element's OWN ems, which are
 * 0.72 of the list's — so the parent-em values are written out beside them.
 * Getting that wrong is what put the number up in the air like a footnote
 * marker instead of on the line beside its text.
 */
.ui-prose ol > li::before {
    content: counter(ui-prose-step);
    position: absolute;
    font-size: 0.72em;
    left: -2.43em;                    /* -1.75em of the list: the gutter */
    top: 0.31em;                      /* (1.7 line-height - 1.25 circle) / 2 */
    width: 1.74em;                    /* 1.25em of the list */
    height: 1.74em;
    line-height: 1.74em;              /* centres the digit in its own circle */
    text-align: center;
    border-radius: 999px;
    background: var(--ui-success-soft);
    color: var(--ui-success-text);
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
/* A nested step is a sub-step: lettered, and quieter than its parent. */
.ui-prose ol ol { counter-reset: ui-prose-substep; }
.ui-prose ol ol > li { counter-increment: ui-prose-substep; }
.ui-prose ol ol > li::before {
    content: counter(ui-prose-substep, lower-alpha);
    background: transparent;
    color: var(--ui-text-faint);
    font-weight: 700;
}

/* ─── Task checklist ────────────────────────────────────────────────── */

/*
 * Each task is a row you could tick, so it is drawn as one: a surface, a box,
 * and a number. Previously they were bare lines of grey text with a small
 * square in front, which read as a paragraph that had been bulleted — the
 * list did not look like the checklist it is describing.
 */
.ui-tasks {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--ui-s-2);
    counter-reset: ui-task;
}
.ui-tasks > li {
    counter-increment: ui-task;
    display: flex;
    align-items: center;
    gap: var(--ui-s-3);
    min-width: 0;
    padding: var(--ui-s-3);
    border-radius: var(--ui-r-md);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
    font-size: 0.875rem;
    line-height: 1.5;
    color: var(--ui-text);
}
/* The step number, not a decorative bullet: a 30-day challenge reads very
   differently when you can see which task is which. */
.ui-tasks > li::before {
    content: counter(ui-task);
    flex: none;
    width: 26px;
    height: 26px;
    display: grid;
    place-items: center;
    border-radius: var(--ui-r-full);
    border: 1px solid var(--ui-tone-line);
    background: var(--ui-tone-soft);
    color: var(--ui-tone-text);
    font-size: 0.75rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

@media (max-width: 640px) {
    .ui-tasks > li { padding: var(--ui-s-2) var(--ui-s-3); gap: var(--ui-s-2); }
}


/* ─── Disclosure ────────────────────────────────────────────────────── */
/*
 * The expandable panel header, used by "Badges You May Earn".
 *
 * It was a row of raw utility classes written out twice — once in server.js
 * for first paint, once in badges.js for the repaint — and the two had
 * already drifted: the server's toggle carried `border-slate-600`, a fixed
 * dark ring that was invisible in dark mode and wrong in light. One class
 * now, used by both, so they cannot disagree again.
 */
.ui-disclosure {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-3);
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    text-align: left;
    color: inherit;
    cursor: pointer;
}
.ui-disclosure:hover .ui-disclosure__icon {
    border-color: var(--ui-tone-line);
    background: var(--ui-tone-soft);
    color: var(--ui-tone-text);
}
.ui-disclosure__toggle {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
    flex: none;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--ui-text-muted);
}
.ui-disclosure__icon {
    width: 26px;
    height: 26px;
    display: grid;
    place-items: center;
    border-radius: var(--ui-r-full);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
    color: var(--ui-text-secondary);
    font-size: 1rem;
    line-height: 1;
    transition: background var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease);
}
@media (max-width: 480px) {
    /* The label is what wraps first; the icon carries the meaning on its own. */
    .ui-disclosure__toggle > span:last-child { display: none; }
}

/* ─── Progress meter ────────────────────────────────────────────────── */

.ui-meter {
    height: 8px;
    border-radius: var(--ui-r-full);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    overflow: hidden;
}
.ui-meter__fill {
    height: 100%;
    background: var(--ui-tone);
    border-radius: inherit;
    transition: width 600ms var(--ui-ease);
}

/* Compact meter used inside challenge cards */
.ui-meter--sm { height: 5px; }

.ui-streak {
    font-size: 2.5rem;
    font-weight: 800;
    letter-spacing: -0.03em;
    color: var(--ui-text);
    margin: var(--ui-s-3) 0;
}

/* ─── Upload dropzone ───────────────────────────────────────────────── */

.ui-dropzone {
    display: block;
    padding: var(--ui-s-8) var(--ui-s-5);
    text-align: center;
    border-radius: var(--ui-r-lg);
    border: 2px dashed var(--ui-border-strong);
    background: var(--ui-surface-3);
    cursor: pointer;
    transition: border-color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-dropzone:hover {
    border-color: var(--ui-tone);
    background: var(--ui-tone-soft);
}
.ui-dropzone__icon { font-size: 1.75rem; line-height: 1; margin-bottom: var(--ui-s-2); }
.ui-dropzone__img { max-height: 220px; margin-inline: auto; border-radius: var(--ui-r-md); object-fit: contain; }

/* ─── Wizard stepper ────────────────────────────────────────────────── */

.ui-step {
    display: flex;
    align-items: center;
    gap: var(--ui-s-3);
    width: 100%;
    padding: 10px 12px;
    text-align: left;
    border-radius: var(--ui-r-md);
    border: 1px solid transparent;
    background: none;
    font: inherit;
    color: var(--ui-text-muted);
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease), color var(--ui-fast) var(--ui-ease);
}
.ui-step:hover { background: var(--ui-surface-3); color: var(--ui-text); }
.ui-step[aria-current="step"] {
    background: var(--ui-tone-soft);
    border-color: var(--ui-tone-line);
    color: var(--ui-tone-text);
}
.ui-step__num {
    width: 24px;
    height: 24px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: 50%;
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
    font-size: 0.6875rem;
    font-weight: 700;
}
.ui-step[aria-current="step"] .ui-step__num,
.ui-step.is-done .ui-step__num {
    background: var(--ui-tone);
    border-color: var(--ui-tone);
    color: #fff;
}
.ui-step__label { font-size: 0.875rem; font-weight: 600; }

/* ─── Badge chips ───────────────────────────────────────────────────── */

.ui-badge-summary {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
}

.ui-badge-tile {
    display: flex;
    align-items: center;
    /* Wraps so the explanation can take a full-width row beneath the badge
       rather than competing with the artwork for horizontal space. */
    flex-wrap: wrap;
    gap: var(--ui-s-3);
    min-width: 0;
    padding: var(--ui-s-3);
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    transition: border-color var(--ui-fast) var(--ui-ease);
}
.ui-badge-tile:hover { border-color: var(--ui-tone-line); }
.ui-badge-tile__art {
    width: 38px;
    height: 38px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: var(--ui-r-md);
    background: var(--ui-tone-soft);
    border: 1px solid var(--ui-tone-line);
    font-size: 1.125rem;
    overflow: hidden;
}
.ui-badge-tile__art img,
.ui-badge-tile__img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
.ui-badge-tile__body { min-width: 0; flex: 1 1 auto; }
.ui-badge-tile__name {
    display: block;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ui-badge-tile__meta { display: block; margin-top: 1px; font-size: 0.6875rem; color: var(--ui-text-muted); }
/* Milestone badges carry a progress track. Guaranteed badges have none —
   nothing to be partway through. */
.ui-badge-tile__progress { display: block; margin-top: 5px; height: 4px; border-radius: 999px; background: var(--ui-border, rgba(148,163,184,0.35)); overflow: hidden; }
.ui-badge-tile__progress-fill { display: block; height: 100%; border-radius: 999px; background: var(--ui-accent, #0284c7); }
/* ─── Info button ───────────────────────────────────────────────────── */
/*
 * The ⓘ on a badge tile. A circle, because that is the shape people already
 * read as "explain this" — a bare "i" in the corner reads as a typo.
 *
 * 30px, not the 22px the tile's other marks use: this one is a TARGET, and
 * the floor for a tap is 34px including the padding around it.
 */
.ui-info {
    flex: none;
    width: 30px;
    height: 30px;
    display: grid;
    place-items: center;
    padding: 0;
    border-radius: var(--ui-r-full);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-1);
    color: var(--ui-text-muted);
    font-family: Georgia, 'Times New Roman', serif;  /* a serif i reads as ⓘ */
    font-size: 0.875rem;
    font-style: italic;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease);
}
.ui-info:hover {
    border-color: var(--ui-tone-line);
    background: var(--ui-tone-soft);
    color: var(--ui-tone-text);
}
.ui-info:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }
.ui-info[aria-expanded="true"] {
    border-color: var(--ui-tone-line);
    background: var(--ui-tone-soft);
    color: var(--ui-tone-text);
}

/*
 * The explanation.
 *
 * Inline and hidden rather than a floating popover: the tiles sit inside
 * `.ui-grid--pair`, and a positioned panel would have to escape its
 * container — which is precisely how the group hero's ⋯ menu ended up
 * clipped. Expanding in place cannot be clipped by anything.
 *
 * The tile is a flex row, so the note is forced onto its own line with a
 * 100% basis rather than being squeezed in beside the artwork.
 */
.ui-badge-tile__note {
    flex: 1 0 100%;
    margin: var(--ui-s-3) 0 0;
    padding-top: var(--ui-s-3);
    border-top: 1px solid var(--ui-border);
    font-size: 0.75rem;
    line-height: 1.6;
    color: var(--ui-text-secondary);
}
.ui-badge-tile__note strong { color: var(--ui-text); font-weight: 700; }

.ui-badge-tile__earned {
    flex: none;
    width: 22px;
    height: 22px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--ui-success);
    color: #fff;
    font-size: 0.75rem;
    font-weight: 700;
}

/* ─── Toast visibility ──────────────────────────────────────────────── */
/* The old page animated three Tailwind classes on and off. One state class
   is easier to reason about and can't get half-applied. */
.ui-toast {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
    opacity: 0;
    transform: translateY(10px);
    pointer-events: none;
    transition: opacity var(--ui-base) var(--ui-ease), transform var(--ui-base) var(--ui-ease);
}
.ui-toast.is-visible { opacity: 1; transform: none; pointer-events: auto; }

/* Helpers used by converted legacy renderers */
.ui-tone-text-c { color: var(--ui-tone-text); }
.ui-tone-soft-c { background: var(--ui-tone-soft); border-color: var(--ui-tone-line); }
.ui-radius-lg   { border-radius: var(--ui-r-lg); }
.ui-required    { color: var(--ui-danger-text); }
.ui-text-danger { color: var(--ui-danger-text); }

/* ─── Action row (Join + Save) ──────────────────────────────────────── */
/*
 * The two answers to the same question — do it now, or keep it for later —
 * so they sit side by side rather than stacked as if one followed the other.
 *
 * The primary grows and the secondary sizes to its label: an equal split
 * would give "Save" the same visual weight as "Join challenge", and they are
 * not equally important.
 */
.ui-action-row {
    display: flex;
    gap: var(--ui-s-2);
    flex-wrap: nowrap;
    align-items: center;
}
.ui-action-row > .ui-btn { min-width: 0; }
.ui-action-row > .ui-btn--primary { flex: 1 1 0; }
.ui-action-row > .ui-btn--secondary,
.ui-action-row > .ui-btn--danger { flex: 0 1 auto; white-space: nowrap; }

@media (max-width: 380px) {
    /* Two buttons sharing 288px puts both under the width where a thumb
       reliably hits one and not the other, so below this they stack. */
    .ui-action-row { flex-wrap: wrap; }
    .ui-action-row > .ui-btn { flex: 1 1 100%; }
}


/* ─── Take-part card ────────────────────────────────────────────────── */
/*
 * Was a card in the right-hand column, beside the leaderboard — the one
 * thing the page exists to make you do, filed with the reference material.
 * It now sits in the reading column between the pitch and the detail.
 */
.ui-join-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-5);
    flex-wrap: wrap;
    border-color: var(--ui-tone-line);
}
/* 240px basis: below that the text and the buttons are both cramped, so the
   card wraps into two rows instead of squeezing. */
.ui-join-card__body { flex: 1 1 240px; min-width: 0; }
.ui-join-card__title { margin: 0 0 4px; font-weight: 700; color: var(--ui-text); }
.ui-join-card .ui-action-row { flex: 0 1 auto; }

@media (max-width: 640px) {
    .ui-join-card .ui-action-row { flex: 1 1 100%; }
}

/* Until actions.js knows whether you have joined, the buttons would show the
   wrong labels. Dimmed and inert rather than hidden, so the card does not
   pop into existence and shift everything under it. */
.ui-join-card[data-actions-ready="false"] .ui-action-row {
    opacity: 0.45;
    pointer-events: none;
}

/* Shown at the moment it applies — joining, or submitting — rather than
   permanently in the sidebar, where it had become furniture nobody read. */
.ui-safety-note {
    font-size: 0.75rem;
    color: var(--ui-text-muted);
}


/* ─── Emoji picker ──────────────────────────────────────────────────── */

.ui-emoji {
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-1);
    padding: var(--ui-s-3);
    /* The grid inside scrolls; this must be able to shrink or it widens its
       parent on a phone instead. */
    min-width: 0;
}
.ui-emoji__head {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
    margin-bottom: var(--ui-s-3);
}
.ui-emoji__search { flex: 1 1 0; min-width: 0; }

.ui-emoji__tabs {
    display: flex;
    gap: var(--ui-s-1);
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: var(--ui-s-2);
    margin-bottom: var(--ui-s-2);
    border-bottom: 1px solid var(--ui-border);
}
.ui-emoji__tabs::-webkit-scrollbar { display: none; }

.ui-emoji__tab {
    flex: none;
    /* 34px is the floor for a tap target; these are icon-only, so there is
       no label to widen them into range. */
    width: 34px;
    height: 34px;
    display: grid;
    place-items: center;
    font-size: 1.0625rem;
    line-height: 1;
    border: 1px solid transparent;
    border-radius: var(--ui-r-md);
    background: none;
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease);
}
.ui-emoji__tab:hover { background: var(--ui-surface-3); }
.ui-emoji__tab[aria-selected="true"] {
    background: var(--ui-tone-soft);
    border-color: var(--ui-tone-line);
}

.ui-emoji__body {
    max-height: 264px;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.ui-emoji__grid {
    display: grid;
    /* auto-fill, not a fixed count: the same picker sits in a 340px sidebar
       and a 720px form, and neither should get a scrollbar it did not need. */
    grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
    gap: 2px;
}

.ui-emoji__cell {
    aspect-ratio: 1;
    display: grid;
    place-items: center;
    font-size: 1.375rem;
    line-height: 1;
    border: 1px solid transparent;
    border-radius: var(--ui-r-md);
    background: none;
    cursor: pointer;
    /* No transition on background: hovering across a grid of 1,100 cells
       would otherwise queue an animation per cell. */
}
.ui-emoji__cell:hover { background: var(--ui-surface-3); }
.ui-emoji__cell:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: -2px; }
.ui-emoji__cell[aria-selected="true"] {
    background: var(--ui-tone-soft);
    border-color: var(--ui-tone-line);
}

.ui-emoji__empty { padding: var(--ui-s-5) var(--ui-s-2); text-align: center; }

/* The chosen emoji, shown as a button that opens the picker. */
.ui-emoji-trigger {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-s-2);
    padding: 6px 12px 6px 8px;
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-inset);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text-secondary);
    cursor: pointer;
    transition: border-color var(--ui-fast) var(--ui-ease);
}
.ui-emoji-trigger:hover { border-color: var(--ui-border-strong); }
.ui-emoji-trigger__face {
    font-size: 1.625rem;
    line-height: 1;
    width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    border-radius: var(--ui-r-md);
    background: var(--ui-tone-soft);
}

/* The same trigger sized to sit beside a text input rather than stand alone.
   The standalone version is 54px tall against a 42px input, which reads as a
   button that has landed on the row by accident. */
.ui-iconfield {
    display: flex;
    align-items: flex-start;
    gap: var(--ui-s-2);
}
.ui-iconfield .ui-input { flex: 1; min-width: 0; }
.ui-iconfield .ui-emoji-trigger { padding: 3px; flex-shrink: 0; }
.ui-iconfield .ui-emoji-trigger__face {
    width: 36px;
    height: 36px;
    font-size: 1.375rem;
}


/* ─── Save bar ──────────────────────────────────────────────────────── */
/*
 * Settings is taller than any screen it will be read on. A Save button at
 * the bottom means editing the name, scrolling past four cards, and hoping
 * the thing you want is down there — and a button that is off-screen while
 * you have unsaved work is how work gets lost on a back-swipe.
 *
 * So it is pinned, and it only appears when there is something to save:
 * a permanent bar is furniture, one that arrives when you change something
 * is a state.
 */
.ui-savebar {
    position: sticky;
    bottom: var(--ui-s-4);
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-3);
    flex-wrap: wrap;
    margin-top: var(--ui-s-6);
    padding: var(--ui-s-3) var(--ui-s-4);
    border-radius: var(--ui-r-lg);
    border: 1px solid var(--ui-tone-line);
    background: var(--ui-surface-1);
    box-shadow: var(--ui-shadow-lg, 0 12px 32px rgba(0, 0, 0, 0.14));
}
.ui-savebar[hidden] { display: none; }
.ui-savebar__text {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text-secondary);
}
.ui-savebar__actions { display: flex; gap: var(--ui-s-2); flex: none; }

@media (max-width: 480px) {
    /* The message and the buttons stop competing for one line; the actions
       stay full width so neither is a 40px target at the screen edge. */
    .ui-savebar { flex-direction: column; align-items: stretch; }
    .ui-savebar__actions > .ui-btn { flex: 1 1 0; }
}
.ui-badge__art  { width: 14px; height: 14px; border-radius: 50%; object-fit: cover; }
.ui-editor      { border: 1px solid var(--ui-border); border-radius: var(--ui-r-md); background: var(--ui-surface-inset); }
.ui-editor .ql-toolbar { border: 0; border-bottom: 1px solid var(--ui-border); }
.ui-editor .ql-container { border: 0; font-family: inherit; font-size: 0.875rem; color: var(--ui-text); }
.ui-editor .ql-editor.ql-blank::before { color: var(--ui-text-faint); font-style: normal; }
/* Paragraph vs line break, in the editor.
   Quill zeroes paragraph margins, so Enter (a new <p>) and Shift+Enter (a
   <br> inside the same <p>) looked identical while typing and then diverged
   once published. These match .ui-prose: a paragraph opens a gap, a line
   break only wraps to the next line.
   Scoped to .ql-editor rather than .ui-editor — initQuillEditor clears the
   container's class, so a .ui-editor-scoped rule never matches.
   The last paragraph keeps its gap too, so the caret does not sit tight
   against the bottom edge of the box while typing. */
.ql-editor p { margin: 0 0 0.75em; }
/* An author typing a list should see the ticks the reader gets, not dots that
   change shape after publishing. Quill 2 draws its own markers into a
   .ql-ui span and marks bullets with data-list, so the override goes there
   rather than on the <li> — the element is an <ol> either way. */
.ui-editor .ql-editor li[data-list="bullet"] > .ql-ui::before {
    content: "\2713";
    color: var(--ui-success);
    font-weight: 700;
}
/* Transient highlight when a panel is scrolled into view */
.is-flash { outline: 2px solid var(--ui-tone); outline-offset: 3px; transition: outline-color var(--ui-base) var(--ui-ease); }
.ui-mt-6 { margin-top: var(--ui-s-6); }
.ui-mb-3 { margin-bottom: var(--ui-s-3); }
.ui-mb-6 { margin-bottom: var(--ui-s-6); }
/* Comment thread indent + misc used by the single-challenge modules */
.ui-thread { margin-top: var(--ui-s-3); padding-left: var(--ui-s-4); border-left: 2px solid var(--ui-border); display: flex; flex-direction: column; gap: var(--ui-s-3); }
.ui-prewrap { white-space: pre-wrap; }
.ui-danger-link { color: var(--ui-danger-text); }
.ui-mb-2 { margin-bottom: var(--ui-s-2); }


/* ─── Online players list ───────────────────────────────────────────── */
/* Used by the arena, the games lobby and profile. Previously two hardcoded
   Tailwind palettes chosen by an `isMidnight` flag, which every caller set
   to true — so the list stayed dark on light pages. */

.ui-player {
    display: flex;
    align-items: center;
    gap: var(--ui-s-3);
    padding: 8px 10px;
    margin-bottom: 6px;
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    transition: border-color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-player:hover { border-color: var(--ui-border-strong); }

.ui-player--match {
    background: var(--ui-accent-soft);
    border-color: var(--ui-accent-line);
}

/* Your own row — tinted with the page tone so it reads as "this is me"
   without shouting louder than the people you can actually challenge. */
.ui-player--self {
    background: var(--ui-tone-soft);
    border-color: var(--ui-tone-line);
    margin-top: var(--ui-s-2);
    margin-bottom: 0;
}

.ui-player__body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.ui-player__name {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ui-player__you { font-weight: 500; color: var(--ui-tone-text); }

.ui-player__status {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--ui-text-muted);
}
.ui-player__status--free .ui-dot { background: var(--ui-success); }
.ui-player__status--busy .ui-dot { background: var(--ui-danger); }
.ui-player__status--busy { color: var(--ui-danger-text); }

/* Checkbox row without the card chrome — for inline filters */
.ui-check--bare {
    padding: 0;
    background: none;
    border: 0;
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--ui-text-muted);
}
.ui-check--bare:hover { background: none; color: var(--ui-text); }
.ui-check--bare:has(input:checked) { background: none; color: var(--ui-tone-text); }

/* Wizard field extras */
.ui-check--start { align-items: flex-start; justify-content: flex-start; text-align: left; }
.ui-dropzone--slim { padding: var(--ui-s-6) var(--ui-s-4); }
.ui-media-preview {
    display: block;
    width: 100%;
    max-height: 200px;
    object-fit: cover;
    border-radius: var(--ui-r-md);
    border: 1px solid var(--ui-border);
}


/* ══════════════════════════════════════════════════════════════════════
   18. GAME SCREENS
   ----------------------------------------------------------------------
   Setup, lobby and result surfaces shared by Connect 4, Word Search,
   Tic-Tac-Toe and Guess My Number.

   The boards themselves keep each game's own palette — those games ship a
   `theme-midnight` variant already, which the product theme now drives.
   ══════════════════════════════════════════════════════════════════════ */

/* ─── Full-screen setup / result overlay ────────────────────────────── */

.ui-screen {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--ui-s-4);
    overflow-y: auto;
    background: color-mix(in srgb, var(--ui-surface-0) 82%, transparent);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
}
.ui-screen.hidden { display: none; }

.ui-screen__panel {
    width: 100%;
    max-width: 880px;
    max-height: calc(100vh - var(--ui-s-8));
    overflow-y: auto;
    padding: var(--ui-s-6);
    border-radius: var(--ui-r-2xl);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    box-shadow: var(--ui-shadow-lg);
}
@media (min-width: 640px) { .ui-screen__panel { padding: var(--ui-s-8); } }

.ui-screen__panel--sm { max-width: 560px; }

/* Account strip at the top of a setup screen */
.ui-screen__account {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--ui-s-3);
    padding-bottom: var(--ui-s-4);
    margin-bottom: var(--ui-s-6);
    border-bottom: 1px solid var(--ui-border);
}

.ui-screen__title {
    margin: 0;
    font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2rem);
    line-height: 1.15;
    font-weight: 800;
    letter-spacing: -0.03em;
    color: var(--ui-text);
}
.ui-screen__sub {
    margin: var(--ui-s-2) 0 0;
    font-size: 0.9375rem;
    line-height: 1.6;
    color: var(--ui-text-secondary);
}

/* Keep targets visible below the sticky navigation after scrollIntoView/hash navigation. */
main [id] { scroll-margin-top: 96px; }

/* ─── Mode picker ───────────────────────────────────────────────────── */
/* Big, obvious choices: play the AI, play locally, play online. */

.ui-modes {
    display: grid;
    gap: var(--ui-s-3);
    grid-template-columns: minmax(0, 1fr);
    margin-top: var(--ui-s-6);
}
@media (min-width: 640px) { .ui-modes { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.ui-mode {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: var(--ui-s-4);
    text-align: left;
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-2);
    border: 1px solid var(--ui-border);
    cursor: pointer;
    font: inherit;
    transition: border-color var(--ui-fast) var(--ui-ease),
                background var(--ui-fast) var(--ui-ease),
                transform var(--ui-fast) var(--ui-ease);
}
.ui-mode:hover { border-color: var(--ui-tone-line); transform: translateY(-2px); }
.ui-mode[aria-pressed="true"],
.ui-mode.is-active {
    border-color: var(--ui-tone);
    background: var(--ui-tone-soft);
}
.ui-mode__icon { font-size: 1.5rem; line-height: 1; }
.ui-mode__name { font-size: 0.9375rem; font-weight: 700; color: var(--ui-text); }
.ui-mode__desc { font-size: 0.8125rem; line-height: 1.5; color: var(--ui-text-muted); }

/* ─── Room code ─────────────────────────────────────────────────────── */

.ui-roomcode {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-3);
    padding: var(--ui-s-4);
    border-radius: var(--ui-r-md);
    background: var(--ui-tone-soft);
    border: 1px dashed var(--ui-tone-line);
}
.ui-roomcode__value {
    font-family: ui-monospace, 'Space Mono', monospace;
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: 0.22em;
    color: var(--ui-tone-text);
}

/* ─── In-game status bar ────────────────────────────────────────────── */

.ui-status {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--ui-s-2);
    padding: 12px var(--ui-s-4);
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    font-size: 0.9375rem;
    font-weight: 700;
    color: var(--ui-text);
    text-align: center;
}

/* Score cards flanking the board */
.ui-score {
    display: flex;
    align-items: center;
    gap: var(--ui-s-3);
    padding: var(--ui-s-3);
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    transition: border-color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-score.is-turn { border-color: var(--ui-tone); background: var(--ui-tone-soft); }
.ui-score__name { font-size: 0.8125rem; font-weight: 600; color: var(--ui-text); }
.ui-score__value {
    margin-left: auto;
    font-size: 1.375rem;
    font-weight: 800;
    color: var(--ui-text);
    font-variant-numeric: tabular-nums;
}

/* ─── Result modal ──────────────────────────────────────────────────── */

.ui-result { text-align: center; }
.ui-result__icon { font-size: 3.5rem; line-height: 1; }
.ui-result__title {
    margin: var(--ui-s-3) 0 0;
    font-size: 1.75rem;
    font-weight: 800;
    letter-spacing: -0.025em;
    color: var(--ui-text);
}
.ui-result__body { margin: var(--ui-s-2) 0 0; font-size: 0.9375rem; line-height: 1.6; color: var(--ui-text-secondary); }

/* Modal open/close animation driven by a state class, so JS toggles one
   thing and the motion stays in CSS with the component. */
.ui-modal--enter, .ui-modal.is-closed { transition: opacity var(--ui-base) var(--ui-ease), transform var(--ui-base) var(--ui-ease); }
.ui-modal.is-closed { opacity: 0; transform: scale(0.94); }

/* Centred, letter-spaced code entry */
.ui-input--code { text-align: center; letter-spacing: 0.3em; text-transform: uppercase; font-family: ui-monospace, monospace; }

/* Scrollable full-screen setup (Word Search has a tall config panel) */
.ui-screen--scroll { align-items: flex-start; padding-block: var(--ui-s-8); }
.ui-screen__panel--split { display: flex; flex-direction: column; gap: var(--ui-s-6); max-width: 900px; }
@media (min-width: 768px) { .ui-screen__panel--split { flex-direction: row; } }
.ui-screen__panel--split > * { flex: 1 1 0; min-width: 0; }

/* Token bridges for converted legacy game markup */
.ui-surface-1-c        { background-color: var(--ui-surface-1); }
.ui-surface-3-c        { background-color: var(--ui-surface-3); }
.ui-border-c           { border-color: var(--ui-border); }
.ui-text-c             { color: var(--ui-text); }
.ui-text-secondary-c   { color: var(--ui-text-secondary); }
.ui-text-muted-c       { color: var(--ui-text-muted); }
.ui-surface-inset-c { background-color: var(--ui-surface-inset); }


/* ══════════════════════════════════════════════════════════════════════
   SPACES — audience membership and content targeting
   ══════════════════════════════════════════════════════════════════════
   Used in two places that must feel like the same idea:
     · the profile panel, where you say which spaces are yours
     · the challenge composer, where you say who a challenge is for
   Same markup, same states, so "Couple" looks like the same object in both.
   ══════════════════════════════════════════════════════════════════════ */

.ui-spaces {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: var(--ui-s-3);
}

/* One selectable space. A <button> rather than a styled checkbox so the whole
   tile is the hit target on touch without a label-for dance. */
.ui-space {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: var(--ui-s-3);
    padding: 13px 14px;
    text-align: left;
    width: 100%;
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    cursor: pointer;
    font: inherit;
    transition: border-color var(--ui-fast) var(--ui-ease),
                background var(--ui-fast) var(--ui-ease),
                box-shadow var(--ui-fast) var(--ui-ease);
}
.ui-space:hover:not([disabled]) { border-color: var(--ui-tone-line); }
.ui-space:focus-visible {
    outline: 2px solid var(--ui-tone);
    outline-offset: 2px;
}
.ui-space[aria-pressed="true"] {
    border-color: var(--ui-tone);
    background: var(--ui-tone-soft);
}

/* Held, but not by direct choice — granted by a life stage or by the platform.
   Shown as on and not editable, because a control that springs back the moment
   you touch it reads as broken rather than as policy. */
.ui-space.is-locked {
    cursor: default;
    border-color: var(--ui-tone-line);
    background: var(--ui-tone-soft);
}
.ui-space.is-locked .ui-space__check { opacity: 0.55; }

/* Unavailable: a space this account cannot hold at all, e.g. Junior. */
.ui-space[disabled]:not(.is-locked) {
    cursor: not-allowed;
    opacity: 0.55;
    background: var(--ui-surface-2);
}

.ui-space__check {
    flex: none;
    width: 20px;
    height: 20px;
    margin-top: 1px;
    border-radius: 6px;
    border: 1.5px solid var(--ui-border-strong, var(--ui-border));
    background: var(--ui-surface-1);
    display: grid;
    place-items: center;
    font-size: 0.75rem;
    line-height: 1;
    color: transparent;
    transition: background var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease);
}
.ui-space[aria-pressed="true"] .ui-space__check,
.ui-space.is-locked .ui-space__check {
    background: var(--ui-tone);
    border-color: var(--ui-tone);
    color: var(--ui-on-accent, #fff);
}

.ui-space__body { min-width: 0; }
.ui-space__name {
    font-size: 0.9375rem;
    font-weight: 650;
    color: var(--ui-text);
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}
.ui-space__desc {
    margin-top: 3px;
    font-size: 0.8125rem;
    line-height: 1.45;
    color: var(--ui-text-muted);
}

/* Why you have this space: "from Married", "everyone", "oversight". */
.ui-space__why {
    display: inline-block;
    margin-top: 6px;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 650;
    letter-spacing: 0.01em;
    color: var(--ui-tone-text);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-tone-line);
}

/* ─── Life-stage chips ──────────────────────────────────────────────── */

.ui-stages {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ui-s-2);
}
.ui-stage {
    padding: 8px 14px;
    border-radius: 999px;
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    color: var(--ui-text);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
    transition: border-color var(--ui-fast) var(--ui-ease),
                background var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease);
}
.ui-stage:hover { border-color: var(--ui-tone-line); }
.ui-stage:focus-visible { outline: 2px solid var(--ui-tone); outline-offset: 2px; }
.ui-stage[aria-pressed="true"] {
    background: var(--ui-tone);
    border-color: var(--ui-tone);
    color: var(--ui-on-accent, #fff);
}

/* ─── Audience badges on content cards ──────────────────────────────── */

.ui-aud-tags {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 4px;
    vertical-align: middle;
}
.ui-aud-tag {
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 650;
    color: var(--ui-tone-text);
    background: var(--ui-tone-soft);
    border: 1px solid var(--ui-tone-line);
    white-space: nowrap;
}


/* ─── Skeletons ─────────────────────────────────────────────────────── */
/*
 * A skeleton is a PREVIEW OF THE THING THAT IS COMING, not a loading widget.
 *
 * The old one was a single 168px rectangle with a shimmer washing across the
 * whole card, which failed on both counts. It did not look like the page —
 * the gradient overrode the card's own surface, so the one element on screen
 * was the one element not wearing the design system — and it did not look
 * like a post either, so when the real content arrived nothing lined up and
 * the layout jumped.
 *
 * So: the CARD is real. Same surface, border, radius, padding and shadow as
 * the post or group card that will replace it, laid out in the same shape.
 * Only the text and image areas are stand-ins, and only they shimmer. The
 * page appears to assemble rather than to flash.
 */
.ui-skeleton {
    display: block;
    border-radius: var(--ui-r-sm);
    background: linear-gradient(90deg,
        var(--ui-surface-3) 25%,
        var(--ui-surface-2) 37%,
        var(--ui-surface-3) 63%);
    background-size: 400% 100%;
    animation: ui-shimmer 1.6s linear infinite;
}
.ui-skeleton--circle { border-radius: var(--ui-r-full); }
.ui-skeleton--text   { height: 11px; }
.ui-skeleton--title  { height: 14px; border-radius: var(--ui-r-sm); }

@keyframes ui-shimmer {
    0%   { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}
/*
 * An infinite animation with no escape is a WCAG 2.2.2 problem, and this one
 * sits under the reader's eyes for the whole load. Reduced motion gets a
 * flat block — still obviously a placeholder, just not moving.
 */
@media (prefers-reduced-motion: reduce) {
    .ui-skeleton { animation: none; background: var(--ui-surface-3); }
}

/* The stand-in for a post: same box, same head row, same body rhythm. */
.ui-skeleton-post__head { display: flex; align-items: center; gap: var(--ui-s-3); }
.ui-skeleton-post__ident { flex: 1 1 auto; min-width: 0; display: grid; gap: 7px; }
.ui-skeleton-post__body { display: grid; gap: 9px; margin-top: var(--ui-s-4); }

/* The stand-in for a sidebar card: a title, then its rows. */
.ui-skeleton-aside__body { display: grid; gap: 10px; margin-top: var(--ui-s-4); }
.ui-skeleton-aside__row { display: flex; align-items: center; gap: var(--ui-s-3); }
.ui-skeleton-aside__lines { flex: 1 1 auto; min-width: 0; display: grid; gap: 7px; }

/* A stand-in for an image or cover strip: the same block, given a height. */
.ui-skeleton--media { height: 11rem; border-radius: 0; }
.ui-skeleton--chip  { height: 18px; width: 84px; border-radius: var(--ui-r-md); }
.ui-skeleton--btn   { height: 28px; width: 76px; border-radius: var(--ui-r-md); }
.ui-skeleton--icon  { height: 20px; width: 20px; border-radius: var(--ui-r-sm); }

/*
 * A grid of skeleton cards. The card itself is real — see the note above —
 * so the grid is the same one the loaded cards land in and nothing shifts
 * when they arrive.
 */
.ui-skeleton-grid {
    display: grid;
    gap: var(--ui-s-4);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
}
.ui-skeleton-grid > * { min-width: 0; }

/* The head, body and foot rows a card skeleton is built from. */
.ui-skeleton-row  { display: flex; align-items: center; justify-content: space-between; gap: var(--ui-s-2); }
.ui-skeleton-ident { display: flex; align-items: center; gap: 10px; }
.ui-skeleton-lines { display: grid; gap: 9px; }

/* Kept as an alias: several places still say `ui-skeleton-card`, and a
   silently unstyled placeholder is worse than a plain one. */
.ui-skeleton-card {
    min-height: 120px;
    border-color: var(--ui-border);
    background: linear-gradient(90deg,
        var(--ui-surface-2) 25%,
        var(--ui-surface-3) 37%,
        var(--ui-surface-2) 63%);
    background-size: 400% 100%;
    animation: ui-shimmer 1.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
    .ui-skeleton-card { animation: none; }
}


/* ─── Group cards ───────────────────────────────────────────────────── */

.ui-group-card {
    display: flex;
    flex-direction: column;
    gap: var(--ui-s-3);
    padding: var(--ui-s-4);
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    text-align: left;
    width: 100%;
    cursor: pointer;
    font: inherit;
    color: inherit;
    transition: border-color var(--ui-fast) var(--ui-ease),
                transform var(--ui-fast) var(--ui-ease),
                box-shadow var(--ui-fast) var(--ui-ease);
}
.ui-group-card:hover {
    border-color: var(--ui-tone-line);
    transform: translateY(-2px);
    box-shadow: var(--ui-shadow-md, 0 8px 24px rgba(0, 0, 0, 0.08));
}
.ui-group-card:focus-visible { outline: 2px solid var(--ui-tone); outline-offset: 2px; }

.ui-group-card__top { display: flex; align-items: flex-start; gap: var(--ui-s-3); }
.ui-group-card__icon {
    flex: none;
    width: 46px;
    height: 46px;
    display: grid;
    place-items: center;
    font-size: 1.375rem;
    border-radius: var(--ui-r-md);
    background: var(--ui-tone-soft);
    border: 1px solid var(--ui-tone-line);
}
.ui-group-card__body { min-width: 0; flex: 1; }
.ui-group-card__name {
    font-size: 1rem;
    font-weight: 700;
    color: var(--ui-text);
    margin: 0;
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}
.ui-group-card__tagline {
    margin: 4px 0 0;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--ui-text-muted);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.ui-group-card__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ui-s-2);
    font-size: 0.75rem;
    color: var(--ui-text-muted);
    font-weight: 600;
}
.ui-group-card__dot { opacity: 0.5; }

/* Group banner on the detail page */
.ui-group-hero {
    position: relative;
    border-radius: var(--ui-r-lg);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-2);
}
/* NOT overflow:hidden.
   The hero used to clip its own contents so the banner picked up the rounded
   corners. That also cropped the ⋯ menu, which is absolutely positioned and
   has to escape the header to be readable — it appeared as a sliver tucked
   under the hero's bottom edge. The banner rounds its own top corners
   instead, so nothing needs clipping at the container level. */
.ui-group-hero__banner {
    height: 150px;
    background: linear-gradient(135deg, var(--ui-tone-soft), var(--ui-surface-3));
    background-size: cover;
    background-position: center;
    border-radius: calc(var(--ui-r-lg) - 1px) calc(var(--ui-r-lg) - 1px) 0 0;
}
.ui-group-hero__body {
    display: flex;
    align-items: flex-start;
    gap: var(--ui-s-4);
    padding: var(--ui-s-4);
    flex-wrap: wrap;
}
.ui-group-hero__icon {
    flex: none;
    width: 64px;
    height: 64px;
    margin-top: -46px;
    display: grid;
    place-items: center;
    font-size: 1.75rem;
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
}

/* ─── Horizontal rail ───────────────────────────────────────────────── */
/* A single-row scroller for "your groups" and similar shortcut strips.
   Scrollbar hidden because the cards are obviously cut off at the edge. */
.ui-rail {
    display: flex;
    gap: var(--ui-s-3);
    overflow-x: auto;
    padding-bottom: var(--ui-s-2);
    scrollbar-width: none;
    -ms-overflow-style: none;
}
.ui-rail::-webkit-scrollbar { display: none; }
.ui-rail > * { flex: 0 0 auto; }

/* Accent-coloured text. The replacement for the old `portal-text`, which
   resolved to a different hue per portal; there is one accent now. */
.ui-accent-text {
    color: var(--ui-accent-text);
    font-weight: 650;
}

/* ─── Stock image picker ────────────────────────────────────────────── */
/* Shared by the challenge composer and the group create wizard. */

/* The stock image picker's own card. A namespace hook so the picker can be
   targeted without reaching for .ui-card, which everything uses. */
.ui-stock { position: relative; padding: var(--ui-s-4); }

.ui-stock__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--ui-s-3);
    margin-bottom: var(--ui-s-3);
}
.ui-stock__hint {
    margin: 2px 0 0;
    font-size: 0.75rem;
    color: var(--ui-text-faint);
}
.ui-stock__close {
    flex: none;
    width: 28px;
    height: 28px;
    display: grid;
    place-items: center;
    border-radius: 999px;
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
    color: var(--ui-text-secondary);
    font-size: 1.125rem;
    line-height: 1;
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease);
}
.ui-stock__close:hover { background: var(--ui-surface-inset); color: var(--ui-text); border-color: var(--ui-tone-line); }
.ui-stock__close:focus-visible { outline: 2px solid var(--ui-tone); outline-offset: 2px; }

/* Search field. The icon sits inside the box rather than beside it, so the
   row reads as one search control instead of two unrelated widgets. */
.ui-stock__search { position: relative; margin-bottom: var(--ui-s-3); }
.ui-stock__search-icon {
    position: absolute;
    left: 12px;
    top: 50%;
    width: 16px;
    height: 16px;
    transform: translateY(-50%);
    pointer-events: none;
    fill: none;
    stroke: var(--ui-text-faint);
    stroke-width: 2;
    stroke-linecap: round;
}
.ui-stock__input {
    width: 100%;
    padding: 10px 14px 10px 36px;
    border-radius: 999px;
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-inset);
    color: var(--ui-text);
    font: inherit;
    font-size: 0.875rem;
    transition: border-color var(--ui-fast) var(--ui-ease),
                box-shadow var(--ui-fast) var(--ui-ease);
}
.ui-stock__input::placeholder { color: var(--ui-text-faint); }
.ui-stock__input:focus {
    outline: none;
    border-color: var(--ui-tone);
    box-shadow: 0 0 0 3px var(--ui-tone-soft, rgba(99, 102, 241, 0.18));
}

/* Sources on the left, shape and colour on the right — one row, so the
   grid starts higher up the panel. Wraps to two rows when narrow. */
.ui-stock__bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-2);
    margin-bottom: var(--ui-s-3);
}
.ui-stock__providers {
    display: inline-flex;
    padding: 3px;
    gap: 2px;
    border-radius: 999px;
    background: var(--ui-surface-inset);
    border: 1px solid var(--ui-border);
}
/* A segmented control, not three loose pills: the choice is one of three. */
.ui-stock__providers .ui-stage {
    padding: 5px 12px;
    border: 0;
    background: transparent;
    font-size: 0.8125rem;
    text-transform: capitalize;
    color: var(--ui-text-secondary);
}
.ui-stock__providers .ui-stage:hover { color: var(--ui-text); }
.ui-stock__providers .ui-stage[aria-pressed="true"] {
    background: var(--ui-surface);
    color: var(--ui-text);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
}
.ui-stock__filters { display: inline-flex; gap: var(--ui-s-2); margin-left: auto; }
.ui-stock__select {
    padding: 7px 12px;
    border-radius: 999px;
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-3);
    color: var(--ui-text-secondary);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
}
.ui-stock__select:hover { border-color: var(--ui-tone-line); color: var(--ui-text); }
.ui-stock__select:focus-visible { outline: 2px solid var(--ui-tone); outline-offset: 2px; }

.ui-stock__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
    gap: var(--ui-s-2);
    max-height: 340px;
    overflow-y: auto;
    padding: 2px;
    scrollbar-width: thin;
}
.ui-stock__note {
    grid-column: 1 / -1;
    padding: var(--ui-s-5);
    text-align: center;
    color: var(--ui-text-faint);
}
.ui-stock__skeleton {
    aspect-ratio: 4 / 3;
    border-radius: var(--ui-r-md);
    background: linear-gradient(90deg, var(--ui-surface-3) 25%, var(--ui-surface-inset) 37%, var(--ui-surface-3) 63%);
    background-size: 400% 100%;
    animation: ui-stock-shimmer 1.4s ease infinite;
}
@keyframes ui-stock-shimmer {
    from { background-position: 100% 0; }
    to   { background-position: 0 0; }
}
@media (prefers-reduced-motion: reduce) {
    .ui-stock__skeleton { animation: none; }
}
.ui-stock__item {
    position: relative;
    padding: 0;
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-md);
    overflow: hidden;
    background: var(--ui-surface-3);
    cursor: pointer;
    aspect-ratio: 4 / 3;
    transition: border-color var(--ui-fast) var(--ui-ease),
                transform var(--ui-fast) var(--ui-ease),
                box-shadow var(--ui-fast) var(--ui-ease);
}
.ui-stock__item:hover {
    border-color: var(--ui-tone);
    transform: translateY(-2px);
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.22);
}
.ui-stock__item:focus-visible { outline: 2px solid var(--ui-tone); outline-offset: 2px; }
.ui-stock__item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform var(--ui-base) var(--ui-ease);
}
.ui-stock__item:hover img { transform: scale(1.04); }
/* Attribution. Kept legible over any photo with its own scrim rather than
   relying on the image being dark. */
.ui-stock__credit {
    position: absolute;
    inset: auto 0 0 0;
    padding: 10px 8px 5px;
    font-size: 0.625rem;
    font-weight: 600;
    color: #fff;
    text-align: left;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0));
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Says what a click does. Hidden until hover so it never covers the photo
   while the reader is still choosing. */
.ui-stock__pick {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    font-size: 0.75rem;
    font-weight: 700;
    color: #fff;
    background: rgba(15, 23, 42, 0.45);
    opacity: 0;
    transition: opacity var(--ui-fast) var(--ui-ease);
}
.ui-stock__item:hover .ui-stock__pick,
.ui-stock__item:focus-visible .ui-stock__pick { opacity: 1; }
@media (hover: none) {
    .ui-stock__pick { display: none; }
    .ui-stock__item:hover img { transform: none; }
}

.ui-stock__foot { display: flex; justify-content: center; margin-top: var(--ui-s-3); }

/* ─── Header image picker ───────────────────────────────────────────── */
/* Used in the group create wizard. A wide preview, because the thing being
   chosen is a banner — a square thumbnail would misrepresent the crop. */

.ui-header-pick {
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-md);
    overflow: hidden;
    background: var(--ui-surface-3);
}
.ui-header-pick__img {
    display: block;
    width: 100%;
    height: 160px;
    object-fit: cover;
}
.ui-header-pick__empty {
    height: 160px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    color: var(--ui-text-muted);
    font-size: 0.8125rem;
    font-weight: 600;
}
.ui-header-pick__empty span:first-child { font-size: 1.75rem; }
.ui-header-pick__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ui-s-2);
    padding: var(--ui-s-3);
    border-top: 1px solid var(--ui-border);
    background: var(--ui-surface-2);
}

/* ─── Group card banner ─────────────────────────────────────────────── */
/* When a group has a header image the card leads with it; the emoji tile is
   only the fallback for groups created before headers existed. */

.ui-group-card__banner {
    display: block;
    width: calc(100% + (2 * var(--ui-s-4)));
    margin: calc(-1 * var(--ui-s-4)) 0 0 calc(-1 * var(--ui-s-4));
    height: 116px;
    object-fit: cover;
    border-radius: var(--ui-r-lg) var(--ui-r-lg) 0 0;
}

/* ─── Overflow menu ─────────────────────────────────────────────────── */
/* Report and Hide used to sit inline beside the reactions, at the same weight
   as a 👍. A rare, consequential action does not belong next to a casual one,
   and on a narrow screen the row wrapped so "Hide" landed under the emoji and
   read as one of them. */
.ui-menu { position: relative; display: inline-flex; }

/* A circular target, so ⋯ reads as a button before you hover it. 34px keeps
   it at the minimum comfortable tap size without looking like a real button
   next to the primary action. */
.ui-menu__trigger {
    width: 34px;
    height: 34px;
    display: grid;
    place-items: center;
    border: 1px solid transparent;
    background: none;
    cursor: pointer;
    color: var(--ui-text-secondary);
    font-size: 1.125rem;
    line-height: 1;
    border-radius: var(--ui-r-full);
    transition: background var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease);
}
.ui-menu__trigger:hover {
    color: var(--ui-text);
    background: var(--ui-surface-2);
    border-color: var(--ui-border);
}
.ui-menu__trigger[aria-expanded="true"] {
    color: var(--ui-text);
    background: var(--ui-surface-2);
    border-color: var(--ui-border);
}

/* A dense row — a comment, a feed item — cannot spare 34px of gutter for a
   control that is only occasionally used. It stays a real tap target on
   touch, where the padding around it is what the finger actually hits. */
.ui-menu__trigger--compact {
    width: 26px;
    height: 26px;
    font-size: 1rem;
    opacity: 0.55;
}
.ui-menu__trigger--compact:hover,
.ui-menu__trigger--compact[aria-expanded="true"] { opacity: 1; }
@media (hover: none) {
    .ui-menu__trigger--compact { width: 32px; height: 32px; opacity: 0.8; }
}

.ui-menu__list {
    /* Absolute is the fallback only. components/ui-menu.js switches this to
       `fixed` with measured coordinates the moment the menu opens, so it
       escapes any scrolling or clipping ancestor — see place() there. */
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    /* Above modals now, not below them. Once the list is position:fixed it is
       no longer a child of the modal's stacking context, so a z-index under
       the modal's 300 put it behind the sheet it was opened from. */
    z-index: 320;
    /* Narrower than before. The old 232-304px let a two-line hint sprawl to
       one long line, which is what made the menu wider than the modal it
       opened in. Hints are meant to wrap. */
    min-width: 210px;
    max-width: min(16.5rem, calc(100vw - 16px));
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--ui-s-1);
    display: grid;
    gap: 1px;
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    /* A literal, not a token: --ui-r-lg is 20px in Junior, which on a 210px
       menu curves the corners past its own first item. */
    border-radius: 13px;
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 12px 28px -6px rgb(0 0 0 / 0.20);
    animation: ui-menu-in var(--ui-fast) var(--ui-ease);
    transform-origin: top right;
}
@keyframes ui-menu-in {
    from { opacity: 0; transform: scale(0.96) translateY(-4px); }
    to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
    .ui-menu__list { animation: none; }
}

.ui-menu__item {
    /* A grid, not a row. An item with a hint is two stacked lines — the
       action, then what it will actually do. As a flex row the hint became a
       sibling of the label and sat beside it, which is why "Hand the room
       over" and "They become host, you become a member" ran together on one
       very wide line. */
    display: grid;
    justify-items: start;
    gap: 1px;
    width: 100%;
    text-align: left;
    border: 0;
    background: none;
    cursor: pointer;
    color: var(--ui-text);
    padding: 0.5rem 0.625rem;
    border-radius: var(--ui-r-sm);
    transition: background var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease);
}

/* The action itself. Wrapping is allowed now that the list has a max-width;
   a long label wraps to two lines instead of forcing a menu wider than the
   screen. */
.ui-menu__label {
    /* 13px, not 14px. A menu is a list of choices scanned in one glance, not
       body copy; the old size made three items look like a paragraph and
       pushed the hint into second-class small print. */
    font-size: 0.8125rem;
    font-weight: 650;
    line-height: 1.3;
    letter-spacing: -0.006em;
}
.ui-menu__item:hover  { background: var(--ui-surface-2); }
.ui-menu__item:active { background: var(--ui-surface-3); }
.ui-menu__item:focus-visible {
    outline: 2px solid var(--ui-accent);
    outline-offset: -2px;
}

/* Destructive items sit last and are ruled off, so "Leave" and "Delete" are
   never the thing your thumb lands on by accident. */
.ui-menu__item--danger { color: var(--ui-danger, #dc2626); }
.ui-menu__item--danger:hover {
    background: color-mix(in srgb, var(--ui-danger, #dc2626) 10%, transparent);
}
/* Spacing and the separator above a destructive item are both the rule's job
   now — .ui-menu__rule, which menuHtml() inserts above the first danger item
   and never between two. The border and margin that used to live here drew a
   second line beside it, with a gap between the two. */

/* A hidden post stays on screen for the moderator who hid it — dimmed, so it
   is obviously not what everyone else sees, with Unhide in reach. */
.ui-post--hidden { opacity: 0.6; border-style: dashed; }

/* ─── Feed ──────────────────────────────────────────────────────────── */
/*
 * The column a list of posts lives in.
 *
 * It exists so both feeds — inside a group, and Group updates — space their
 * cards identically. They were using different things: ui-stack-sm (8px) in
 * one and Tailwind's space-y-4 (16px) in the other, so the same post looked
 * differently spaced depending on where you read it, and 8px was tight enough
 * that two cards read as one.
 *
 * 24px is deliberately more than the design system's default rhythm. These
 * are whole self-contained blocks with their own internal padding and a
 * border; the gap has to beat that internal spacing or the eye groups a
 * card's footer with the next card's header.
 */
.ui-feed {
    display: grid;
    gap: var(--ui-s-6);
}
/* Same reason as .ui-shell: a grid item's default minimum is its content
   width, so one long unbroken string in one post would widen the whole feed. */
.ui-feed > * { min-width: 0; }
@media (max-width: 640px) {
    /* Narrower screen, less room to spend — but still clearly more than the
       12px of padding inside each card. */
    .ui-feed { gap: var(--ui-s-4); }
}

/* ─── Toggle ────────────────────────────────────────────────────────── */
/*
 * A switch, for settings that are ON or OFF right now.
 *
 * These were checkboxes labelled "Turn off reactions" and "Turn off comments" —
 * negatives, unticked, meaning the thing was ON. You had to read the label,
 * invert it, then invert it again against the empty box to work out the
 * current state. A switch shows the state itself, so the label can name the
 * thing rather than describe an action against it.
 *
 * The input stays in the DOM and focusable rather than display:none, which
 * would drop it out of the tab order and stop the label toggling it.
 */
.ui-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-s-2);
    cursor: pointer;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text-secondary);
    user-select: none;
}
.ui-toggle input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    margin: 0;
}
.ui-toggle__track {
    position: relative;
    flex: none;
    width: 36px;
    height: 20px;
    border-radius: var(--ui-r-full);
    background: var(--ui-surface-3);
    border: 1px solid var(--ui-border);
    transition: background var(--ui-fast) var(--ui-ease),
                border-color var(--ui-fast) var(--ui-ease);
}
.ui-toggle__track::after {
    content: '';
    position: absolute;
    top: 1px;
    left: 1px;
    width: 16px;
    height: 16px;
    border-radius: var(--ui-r-full);
    background: #fff;
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.28);
    transition: transform var(--ui-fast) var(--ui-ease);
}
.ui-toggle input:checked + .ui-toggle__track {
    background: var(--ui-accent);
    border-color: var(--ui-accent);
}
.ui-toggle input:checked + .ui-toggle__track::after { transform: translateX(16px); }
/* The label brightens when on, so state reads without decoding the colour of
   a 36px track — useful for anyone who cannot separate the two greys. */
.ui-toggle input:checked ~ .ui-toggle__label { color: var(--ui-text); }
.ui-toggle input:focus-visible + .ui-toggle__track {
    outline: 2px solid var(--ui-accent);
    outline-offset: 2px;
}
.ui-toggle input:disabled + .ui-toggle__track { opacity: 0.5; }
@media (prefers-reduced-motion: reduce) {
    .ui-toggle__track, .ui-toggle__track::after { transition: none; }
}

/* ─── Post ──────────────────────────────────────────────────────────── */
/*
 * The social-feed post layout: a header row, then content at FULL card width.
 *
 * The body used to be nested in a flex row beside the avatar, so every line of
 * text started 52px in and the readable width shrank by that much — worst on a
 * phone, where it mattered most. Facebook, Twitter and LinkedIn all put
 * identity on one row and then let the content span the card, for that reason.
 *
 * Padding lives on the post and the media cancels it with negative margins, so
 * images run edge to edge while text stays inset.
 */
.ui-post {
    --ui-post-pad: var(--ui-s-4);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-xl);
    padding: var(--ui-post-pad);
    overflow: hidden;
}

.ui-post__head {
    display: flex;
    align-items: flex-start;
    gap: var(--ui-s-3);
    margin-bottom: var(--ui-s-3);
}
/* Identity takes the slack, so the ⋯ is pinned right and a long name
   truncates rather than pushing it off the card. */
.ui-post__ident { flex: 1; min-width: 0; }
.ui-post__title {
    font-weight: 700;
    font-size: 0.9375rem;
    line-height: 1.25;
    color: var(--ui-text);
    text-decoration: none;
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
a.ui-post__title:hover { text-decoration: underline; }
.ui-post__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 1px;
    font-size: 0.75rem;
    color: var(--ui-text-secondary);
}
.ui-post__dot { opacity: 0.6; }

.ui-post__body {
    font-size: 0.9375rem;
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    color: var(--ui-text);
}

/* Edge to edge. The negative margin is the post's own padding, so this stays
   correct when the padding changes at the mobile breakpoint below. */
.ui-post__media {
    margin: var(--ui-s-3) calc(var(--ui-post-pad) * -1) 0;
    display: grid;
    gap: 2px;
}
.ui-post__media--pair { grid-template-columns: 1fr 1fr; }
.ui-post__media img {
    width: 100%;
    display: block;
    object-fit: cover;
    max-height: 420px;
}
.ui-post__media--pair img { max-height: 240px; }

/* Reactions sit under a rule, as they do in every feed people already know
   how to read. */
.ui-post__actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--ui-s-2);
    margin-top: var(--ui-s-3);
    padding-top: var(--ui-s-3);
    border-top: 1px solid var(--ui-border);
}

/*
 * The comments summary joins this row rather than opening a second one.
 *
 * display:contents dissolves the wrapper so its two children become items of
 * this flex row: the toggle sits at the far end beside the reactions, and the
 * panel — once opened — wraps onto its own full-width line beneath. Without
 * it the comments block added a second horizontal rule, so a post with one
 * sentence got text, line, reactions, line, "Comment".
 */
.ui-post__actions .ui-comments { display: contents; }
.ui-post__actions [data-comments-toggle] { margin-left: auto; }
.ui-post__actions .ui-comments__panel { flex-basis: 100%; margin-top: var(--ui-s-3); }

@media (max-width: 640px) {
    /*
     * The card stays inset on a phone: it keeps the page's left and right
     * padding, its rounded corners and its side borders.
     *
     * This briefly went edge-to-edge, copying the native Facebook app. That
     * works there because the feed IS the whole screen; here the card sits in
     * a page with its own margins, and a post touching both edges read as a
     * layout fault rather than a deliberate full-bleed — it also put tappable
     * controls hard against the screen edge, where thumbs and browser
     * back-swipe gestures fight.
     *
     * Only the internal padding tightens, buying back room for text without
     * moving the card.
     */
    .ui-post {
        --ui-post-pad: var(--ui-s-3);
    }
    .ui-post__media img { max-height: 340px; }
    /* Two portrait images side by side on a phone are two unreadable slivers. */
    .ui-post__media--pair { grid-template-columns: 1fr; }
    .ui-post__media--pair img { max-height: 300px; }
}

/* ─── Comments ──────────────────────────────────────────────────────── */
/* Collapsed by default. A feed with every thread open is unreadable, and the
   count on the toggle is accurate before anything is fetched because it ships
   with the post. */
/*
 * No rule of its own.
 *
 * The comments block used to add a second horizontal line, so a post with one
 * sentence got: text, line, reactions, line, "Comment". Two dividers to
 * separate three things, one of which was a word. A divider should mark a
 * change of kind — content to actions — and there is only one of those.
 *
 * The summary now joins the action row instead (see .ui-post__actions).
 */
.ui-comments { margin-top: var(--ui-s-3); }
.ui-comments__panel { margin-top: var(--ui-s-3); }
.ui-comments__list {
    display: grid;
    gap: var(--ui-s-3);
    margin-bottom: var(--ui-s-3);
}

.ui-comment {
    display: flex;
    align-items: flex-start;
    gap: var(--ui-s-2);
}
.ui-comment__body { flex: 1; min-width: 0; }

/* The bubble is what separates one comment from the next without a divider
   for every line — at this density, borders everywhere become noise. */
.ui-comment__bubble {
    background: var(--ui-surface-2);
    border-radius: var(--ui-r-md);
    padding: var(--ui-s-2) var(--ui-s-3);
}
.ui-comment__text {
    font-size: 0.875rem;
    margin-top: 2px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.ui-comment__meta {
    gap: var(--ui-s-2);
    margin-top: 4px;
}
/* One level of nesting only — see migration 028. The rail makes the
   relationship readable without relying on indentation alone. */
.ui-comment__replies {
    display: grid;
    gap: var(--ui-s-3);
    margin-top: var(--ui-s-3);
    padding-left: var(--ui-s-3);
    border-left: 2px solid var(--ui-border);
}
/* A reply sits one step quieter than the comment it answers, so a long thread
   still reads as a hierarchy when the left rail scrolls out of view. */
.ui-comment--reply .ui-comment__bubble { background: var(--ui-surface-3); }
.ui-comment--reply .ui-comment__text { font-size: 0.8125rem; }

/* A hidden comment stays visible to moderators, dimmed, so they can see what
   they have taken down and put it back. */
.ui-comment--hidden { opacity: 0.55; }

.ui-comment-form {
    display: flex;
    gap: var(--ui-s-2);
    align-items: center;
    margin-top: var(--ui-s-2);
}
.ui-comment-form .ui-input { flex: 1; min-width: 0; }

/* ─── Reactions ─────────────────────────────────────────────────────── */
/* Small and quiet until used. A reaction is a low-stakes gesture; sizing it
   like a primary button asks for more commitment than it deserves. */
/* ─── Online players ────────────────────────────────────────────────── */
/* The selected card also sets an inline border colour and ring; this carries
   the part that does not depend on the runtime accent. */
.ui-player--selected { background: var(--ui-surface-2); }

/* Three dots that breathe while an invitation is outstanding — a spinner
   would claim the app is working, when it is the other player we are waiting
   on. */
.ui-match-pending {
    display: inline-flex;
    align-items: center;
    gap: 3px;
}
.ui-match-pending span {
    width: 5px;
    height: 5px;
    border-radius: var(--ui-r-full);
    background: currentColor;
    opacity: 0.35;
    animation: ui-match-pending 1.2s ease-in-out infinite;
}
.ui-match-pending span:nth-child(2) { animation-delay: 0.18s; }
.ui-match-pending span:nth-child(3) { animation-delay: 0.36s; }
@keyframes ui-match-pending {
    0%, 60%, 100% { opacity: 0.35; transform: translateY(0); }
    30%           { opacity: 1;    transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
    .ui-match-pending span { animation: none; opacity: 0.6; }
}

.ui-react {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-s-1);
    padding: 4px var(--ui-s-2);
    min-height: 30px;
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-full);
    background: var(--ui-surface-1);
    font-size: 0.8125rem;
    line-height: 1;
    cursor: pointer;
    transition: border-color var(--ui-fast) var(--ui-ease), background var(--ui-fast) var(--ui-ease);
}
.ui-react:hover { border-color: var(--ui-accent); background: var(--ui-surface-2); }
/* Smaller on comments: a reaction to a reply should not outweigh the reply. */
.ui-react--xs { min-height: 24px; padding: 2px var(--ui-s-2); font-size: 0.75rem; }
.ui-react:disabled { opacity: 0.6; cursor: default; }
.ui-react--on {
    border-color: var(--ui-accent);
    background: var(--ui-accent-soft);
    font-weight: 700;
}
.ui-react__count {
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--ui-text-secondary);
    font-variant-numeric: tabular-nums;
}
.ui-react--on .ui-react__count { color: var(--ui-accent); }

/* ─── Group hero ────────────────────────────────────────────────────── */
/* The hero carries identity only: banner, name, one-line tagline, and the
   single action a visitor is here to take. Everything factual — category,
   privacy, member count, your role, the invite code — moved to the sidebar.
   A row of five pills under the title read as decoration, not information,
   and pushed the actual content below the fold. */
.ui-group-hero__head {
    flex: 1;
    min-width: 0;
}
.ui-group-hero__tagline {
    margin-top: var(--ui-s-1);
    color: var(--ui-text-secondary);
    font-size: 0.9375rem;
}
.ui-group-hero__actions {
    display: flex;
    gap: var(--ui-s-2);
    flex: none;
    align-items: center;
}

/* ─── Meta list ─────────────────────────────────────────────────────── */
/* Label/value pairs in a sidebar card. A <dl> rather than badges: these are
   facts with names, and "General" or "1 members" floating in a pill says
   nothing about what it is measuring. */
.ui-meta-list {
    display: grid;
    gap: var(--ui-s-3);
    margin: 0;
}
.ui-meta-list__row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--ui-s-4);
}
.ui-meta-list__row dt {
    color: var(--ui-text-secondary);
    font-size: 0.8125rem;
    flex: none;
}
.ui-meta-list__row dd {
    margin: 0;
    font-weight: 600;
    font-size: 0.875rem;
    text-align: right;
    min-width: 0;
}

/* ─── Copy field ────────────────────────────────────────────────────── */
/* A value you are meant to send to someone else. It used to be plain text
   you had to select by hand. */
.ui-copy-field {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
    padding: var(--ui-s-2) var(--ui-s-3);
    border: 1px dashed var(--ui-border);
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-2);
}
.ui-copy-field__value {
    flex: 1;
    min-width: 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-weight: 700;
    letter-spacing: 0.08em;
    overflow-wrap: anywhere;
}


/* ─── Signed-in / signed-out visibility ─────────────────────────────── */
/*
   The page used to render its signed-OUT state, fetch the session, then swap.
   On every load a logged-in person saw the sign-in card flash first.

   `data-auth` is set on <html> by a blocking script in <head>, from the
   JS-readable `pibera_logged_in` cookie the server sets at login. That happens
   before first paint, so the right half is chosen from the start. The session
   fetch then sets the same attribute authoritatively, which corrects the guess
   if the cookie outlived the session.

   Until the attribute exists (JS disabled, or the head script failed) both
   halves stay visible rather than hiding everything — a broken guess must
   never leave a blank page.
*/
html[data-auth="in"]  [data-when="signed-out"] { display: none !important; }
html[data-auth="out"] [data-when="signed-in"]  { display: none !important; }

/* ─── Footer theme switch ───────────────────────────────────────────── */
/*
   The light/dark control moved out of the nav. In the header it sat beside the
   profile menu and the burger, competing for attention with things people
   actually navigate by — while being something most set once and never touch.
   The footer is where that kind of preference belongs.
*/

/* Pushes the switch and the copyright to the end of the footer row. */
.ui-footer__end {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: var(--ui-s-4);
    flex-wrap: wrap;
}

.ui-theme-switch {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-s-2);
    padding: 7px 13px;
    border-radius: var(--ui-r-full);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-2);
    color: var(--ui-text-muted);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
    transition: border-color var(--ui-fast) var(--ui-ease),
                color var(--ui-fast) var(--ui-ease),
                background var(--ui-fast) var(--ui-ease);
}
.ui-theme-switch:hover {
    border-color: var(--ui-tone-line);
    color: var(--ui-text);
    background: var(--ui-surface-3);
}
.ui-theme-switch:focus-visible {
    outline: 2px solid var(--ui-tone);
    outline-offset: 2px;
}

/* The label is helpful on desktop and just noise on a narrow phone footer,
   where the ◐ alone is understood. */
@media (max-width: 480px) {
    .ui-theme-switch__label { display: none; }
    .ui-theme-switch { padding: 7px 10px; }
}

/* ─── ...and above 900px it is not in the footer at all ─────────────── */
/*
   The footer switch and the account menu's Appearance row are the SAME
   control in two places, and two of them is one too many: the footer is the
   bottom of the page, so setting the theme meant scrolling past everything
   to reach a preference that has nothing to do with where you are.

   From 900px up the hamburger is on screen at all times, and that is where
   this now lives. Below 900px nothing changes: .ui-nav (and its hamburger) is
   hidden, the app surface uses the More sheet, and the landing pages keep the
   footer switch as their only way to change theme.

   Gated on the app nav rather than on width alone, because a legacy page with
   no hamburger to move the control INTO must keep the one in its footer. */
@media (min-width: 900px) {
    body[data-nav="app"] .ui-footer .ui-theme-switch { display: none; }
}

/* ─── Challenge card states ─────────────────────────────────────────── */
/*
   In-progress and completed challenges sit in the SAME grid as everything
   else — they are not a separate section any more. So they have to be
   recognisable at a glance without breaking the row rhythm: same size, same
   position in the flow, different surface.

   Everything below is expressed in tokens, so both themes follow
   automatically. The accent tint is mixed against --ui-surface-1 rather than
   hardcoded, which is what stops it turning into a muddy block in dark mode.
*/

.ui-tile--active {
    border-color: var(--ui-tone);
    background:
        linear-gradient(
            180deg,
            color-mix(in srgb, var(--ui-tone) 7%, var(--ui-surface-1)) 0%,
            var(--ui-surface-1) 45%
        );
    box-shadow: var(--ui-shadow-md, 0 6px 20px rgba(0, 0, 0, 0.08));
}
/* A spine down the leading edge: reads as "yours" without shouting. */
.ui-tile--active::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
    background: var(--ui-tone);
    border-radius: var(--ui-r-lg) 0 0 var(--ui-r-lg);
}
.ui-tile--active { position: relative; }

.ui-tile--done {
    border-color: var(--ui-border);
    background: var(--ui-surface-2);
}
/* Finished work should recede, not compete with what still needs doing. */
.ui-tile--done .ui-tile__media { opacity: 0.72; }
.ui-tile--done .ui-tile__title a { color: var(--ui-text-muted); }

/* ─── State pill ────────────────────────────────────────────────────── */
/* Sits in the card foot next to the action, so the state and the thing you
   can do about it are read together. */

.ui-state-pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 10px;
    border-radius: var(--ui-r-full);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    white-space: nowrap;
    color: var(--ui-tone-text);
    background: var(--ui-tone-soft);
    border: 1px solid var(--ui-tone-line);
}
.ui-state-pill::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
}
.ui-state-pill--done {
    color: var(--ui-text-muted);
    background: var(--ui-surface-3);
    border-color: var(--ui-border);
}

/* The foot holds an action and a pill; keep them apart on a narrow card. */
.ui-tile__foot { flex-wrap: wrap; align-items: center; gap: var(--ui-s-2); }

/* ─── Challenge actions: no flash of the wrong buttons ──────────────── */
/*
   The Join / Leave / Save buttons live in the static markup, so they painted
   in their default "not joined" state before the boot script ran. Someone
   opening a challenge they had already completed saw "Join challenge" and
   "Save" flash past, then get replaced by "Completed".

   renderActions() sets data-actions-ready="true" once the participant state is
   known — the attribute already existed and styled nothing. Until then the
   controls are held back and a short line explains the pause.
*/
#challenge-actions[data-actions-ready="false"] #join-btn,
#challenge-actions[data-actions-ready="false"] #leave-btn,
#challenge-actions[data-actions-ready="false"] #follow-btn,
#challenge-actions[data-actions-ready="false"] #join-helper,
#challenge-actions[data-actions-ready="false"] #join-question {
    display: none !important;
}
#challenge-actions[data-actions-ready="false"]::after {
    content: "Checking your progress…";
    display: block;
    padding: var(--ui-s-3) 0;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ui-text-muted);
}

/* ─── Completion report ─────────────────────────────────────────────── */
/*
   Shown in place of the progress form once a challenge is finished. A dated
   list of what was actually logged: written entries in full, uploaded proof
   as images, or simply the date when the challenge only asked for a tick.
*/

.ui-report {
    list-style: none;
    margin: 0;
    padding: 0;
    position: relative;
}
/* A timeline spine, so a run of entries reads as a sequence. */
.ui-report::before {
    content: "";
    position: absolute;
    left: 5px;
    top: 6px;
    bottom: 6px;
    width: 2px;
    background: var(--ui-border);
}

.ui-report__entry {
    position: relative;
    padding: 0 0 var(--ui-s-4) var(--ui-s-6);
}
.ui-report__entry::before {
    content: "";
    position: absolute;
    left: 0;
    top: 6px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--ui-tone);
    border: 2px solid var(--ui-surface-1);
}
.ui-report__entry.is-flagged::before { background: var(--ui-text-muted); }
.ui-report__entry:last-child { padding-bottom: 0; }

.ui-report__when {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
    flex-wrap: wrap;
    margin-bottom: 4px;
}
.ui-report__date {
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--ui-text-muted);
    letter-spacing: 0.01em;
}
.ui-report__text {
    margin: 0;
    font-size: 0.875rem;
    line-height: 1.6;
    color: var(--ui-text);
    white-space: pre-wrap;   /* keep the line breaks people typed */
}
.ui-report__note {
    margin: 6px 0 0;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--ui-text-muted);
}
.ui-report__proof {
    display: block;
    margin-top: var(--ui-s-2);
    max-width: 240px;
    border-radius: var(--ui-r-md);
    overflow: hidden;
    border: 1px solid var(--ui-border);
}
.ui-report__proof img { display: block; width: 100%; height: auto; }

/* The checklist the challenge asked for, all ticked. */
.ui-report__tasks {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.ui-report__tasks li {
    display: flex;
    align-items: flex-start;
    gap: var(--ui-s-2);
    font-size: 0.875rem;
    color: var(--ui-text);
}
.ui-report__tick {
    flex: none;
    width: 18px;
    height: 18px;
    margin-top: 1px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--ui-tone-soft);
    color: var(--ui-tone-text);
    font-size: 0.6875rem;
    font-weight: 800;
}

/* The panel reads as a record rather than a form once complete. */
#progress-panel[data-state="complete"] { border-color: var(--ui-tone-line); }
.ui-report__body { min-width: 0; }

/* ─── Tile tag row ──────────────────────────────────────────────────── */
/*
   Category on the left, spaces on the right, across the top of the image.

   The space tags used to sit in a meta row inside the card body, which cost a
   whole line for two short words and pushed the title down. Up here they use
   space the cover photo was already occupying.
*/
.ui-tile__tags {
    position: absolute;
    inset: 10px 10px auto 10px;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--ui-s-2);
    pointer-events: none;   /* the whole media is one link */
}
/* Now positioned by the row, not by itself. */
.ui-tile__tags .ui-tile__tag {
    position: static;
    inset: auto;
}
/* Space tags need the same glass treatment as the category chip, or they are
   unreadable over a light photo. */
.ui-tile__tags .ui-aud-tags {
    display: inline-flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 4px;
}
.ui-tile__tags .ui-aud-tag {
    background: color-mix(in srgb, var(--ui-surface-1) 88%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-color: var(--ui-border);
    color: var(--ui-text-secondary);
    font-size: 0.625rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 4px 9px;
}

/* ─── Challenge rating ──────────────────────────────────────────────── */
/* Shown only on a finished challenge, inside the completion report. */

.ui-rating-block {
    margin-top: var(--ui-s-5);
    padding-top: var(--ui-s-4);
    border-top: 1px solid var(--ui-border);
}
.ui-star-row {
    display: flex;
    align-items: center;
    gap: 2px;
    margin-top: var(--ui-s-2);
}
.ui-star {
    padding: 2px 3px;
    border: none;
    background: none;
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    color: var(--ui-border-strong, var(--ui-border));
    transition: color var(--ui-fast) var(--ui-ease), transform var(--ui-fast) var(--ui-ease);
}
.ui-star:hover { transform: scale(1.12); }
.ui-star:focus-visible { outline: 2px solid var(--ui-tone); outline-offset: 2px; border-radius: 4px; }
.ui-star.is-on { color: #f5a524; }
/* Hovering a star lights it and everything before it, the way a rating
   control is expected to behave. Pure CSS — no mouseover bookkeeping. */
.ui-star-row:hover .ui-star { color: #f5a524; }
.ui-star-row .ui-star:hover ~ .ui-star { color: var(--ui-border-strong, var(--ui-border)); }

.ui-star-row #rating-state {
    margin-left: var(--ui-s-2);
    color: var(--ui-text-muted);
}


/* ─── Quest creator ─────────────────────────────────────────────────── */

/* The wizard stepper laid out as a horizontal tab row above the form.
   .ui-step is width:100% for the old vertical aside, so it is re-based
   here rather than given a second set of colours. */
.ui-tabset--steps {
    /* Wraps rather than scrolls: a step that has slid off the right edge is
       a step the author does not know exists. */
    flex-wrap: wrap;
    overflow-x: visible;
    gap: var(--ui-s-2);
    margin-bottom: var(--ui-s-4);
    padding: var(--ui-s-2);
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-2);
    border: 1px solid var(--ui-border);
}
.ui-tabset--steps > .ui-step {
    flex: 1 1 150px;
    width: auto;
    justify-content: center;
    white-space: nowrap;
}
@media (max-width: 640px) {
    .ui-tabset--steps > .ui-step { flex: 0 0 auto; }
    .ui-tabset--steps .ui-step__label { display: none; }
}

/* The live preview follows the author down a long step rather than
   scrolling off at the first field. */
.ui-create-preview { display: block; }
@media (min-width: 1024px) {
    .ui-create-preview {
        position: sticky;
        top: var(--ui-s-6);
        max-height: calc(100vh - 96px);
        overflow-y: auto;
        scrollbar-width: thin;
    }
}

/* Four unit modes side by side instead of the default single column. */
.ui-modes--compact {
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    margin-top: var(--ui-s-2);
}

/* ─── Content builder: checkpoint & media repeater ────────────────────
   The quest composer's unit card needs two things the kit did not have:
   a success-toned small action ("+ Add Checkpoint", which is additive,
   not primary) and a borderless danger icon for removing one row of a
   repeater — a full "Remove" button at every row out-shouted the field
   it was attached to. */
.ui-btn--success {
    background: var(--ui-success-soft);
    border-color: color-mix(in srgb, var(--ui-success) 30%, transparent);
    color: var(--ui-success-text);
}
.ui-btn--success:hover:not(:disabled) {
    background: color-mix(in srgb, var(--ui-success) 22%, transparent);
    border-color: color-mix(in srgb, var(--ui-success) 45%, transparent);
}

.ui-iconbtn--danger {
    width: 34px;
    height: 34px;
    background: none;
    border-color: transparent;
    color: var(--ui-danger-text);
    font-size: 0.85rem;
}
.ui-iconbtn--danger:hover {
    background: var(--ui-danger-soft);
    border-color: var(--ui-danger-line);
    color: var(--ui-danger-text);
}

/* Repeater row: index, field, remove — the field takes the slack. */
.ui-repeat-row { display: flex; align-items: center; gap: var(--ui-s-2); }
.ui-repeat-row > .ui-input { flex: 1 1 auto; min-width: 0; }
.ui-repeat-index {
    flex: none;
    font-size: 0.75rem;
    font-weight: 800;
    color: var(--ui-text-muted);
    min-width: 1.25rem;
}

/* Two-line checkbox label: name on top, what it asks for underneath. */
.ui-check--start > span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ui-check--start > span > .ui-strong { font-size: 0.78rem; line-height: 1.2; }

/* Menu extras: the rule above a destructive action, and the quiet second
   line an item sometimes needs to say what it will actually do. */
.ui-menu__rule {
    height: 1px;
    margin: 5px 0.375rem;
    background: var(--ui-border);
}
.ui-menu__hint {
    display: block;
    font-size: 0.6875rem;
    font-weight: 500;
    line-height: 1.45;
    color: var(--ui-text-muted);
    /* The hint belongs to the label above it, so it reads as a second line of
       one item rather than as a separate menu row. */
    text-wrap: pretty;
}

/* A danger item's hint stays neutral. Tinting it red made two weights of red
   in one item and dropped the contrast on the sentence that most needs
   reading — the label alone carries the warning. */
.ui-menu__item--danger .ui-menu__hint { color: var(--ui-text-muted); }


/* ─── Rich editor (components/rich-editor.js) ───────────────────────────── */
/* The quest description editor: Quill's own toolbar plus one appended icon
   button and a panel that drops under it for images, video links and emoji.
   The panel is inside the editor's border rather than a floating popover, so
   it cannot be clipped by the modal the wizard runs in. */
.ui-rich { position: relative; }
/* Quill takes over the field node and appends .ql-container to whatever class
   it already carries, so the class is reset to this one first — a leftover
   layout class there fights the editor's own box. */
.ui-rich__surface { border: 0; font-family: inherit; font-size: 0.875rem; color: var(--ui-text); }
.ui-rich .ql-toolbar { display: flex; flex-wrap: wrap; align-items: center; }
.ui-rich__emoji-btn {
    width: 28px; height: 24px; padding: 0;
    background: none; border: 0; cursor: pointer;
    font-size: 0.95rem; line-height: 1; color: var(--ui-text-secondary);
}
.ui-rich__emoji-btn:hover { color: var(--ui-tone-text); }
.ui-rich__panel {
    border-bottom: 1px solid var(--ui-border);
    background: var(--ui-surface);
    padding: var(--ui-s-3);
}
.ui-rich__panel-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--ui-s-2); margin-bottom: var(--ui-s-2);
}
.ui-rich__choices { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-s-2); }
/* The upload control is a <label> wrapping a hidden <input type=file>, so it
   needs the alignment a real button gets from .ui-btn. */
.ui-rich__choices > label.ui-btn { cursor: pointer; }
.ui-rich__mount:not(:empty) { margin-top: var(--ui-s-3); }

/* What the author sees while typing, matching how the reader gets it. */
.ql-editor img { max-width: 100%; height: auto; border-radius: var(--ui-r-md); }
.ql-editor iframe.ql-video { width: 100%; aspect-ratio: 16 / 9; height: auto; border: 0; border-radius: var(--ui-r-md); }

/* …and the published side. Both are inserted by the sanitiser in
   components/rich-text.js, which sets these two classes itself. */
.ui-prose__image { display: block; max-width: 100%; height: auto; border-radius: var(--ui-r-md); margin: 0 0 0.75em; }
.ui-prose__video { display: block; width: 100%; aspect-ratio: 16 / 9; height: auto; border: 0; border-radius: var(--ui-r-md); margin: 0 0 0.75em; }

/* ── Milestone disclosure ───────────────────────────────────────────
   A closed-by-default list of the badges a quest hands out along the
   way. Built on <details> rather than a scripted panel: it opens with a
   keyboard, it is announced as expandable, and it is correct with no JS.
   The native triangle is replaced by the +/− on the right, which rotates
   into an × when open. Defined here rather than in the quest page's own
   <style>, so the class the front end asks for has a rule behind it. */
.ui-milestones > summary { list-style: none; cursor: pointer; }
.ui-milestones > summary::-webkit-details-marker { display: none; }
.ui-milestones[open] .ui-milestones__icon { transform: rotate(45deg); }
.ui-milestones__icon { transition: transform .18s ease; }

/* ─── 20. INSTALLED APP (PWA) ────────────────────────────────────────────
   Only the INSTALLED app needs the top and bottom insets. An earlier version
   of this block applied them unconditionally, on the assumption that the
   insets are zero unless the hardware has a notch. That is wrong once
   viewport-fit=cover is on the viewport tag — which add-pwa-tags.js puts on
   every page. viewport-fit=cover is precisely the instruction to lay the page
   out edge to edge, *including* underneath the status bar, so the insets go
   non-zero in an ordinary browser tab too. There the browser's own toolbar
   already sits in that space, so the padding was pure dead space above the
   app bar: a visible gap on every phone.

   Standalone has no browser chrome, so there the padding is real. */
@media (display-mode: standalone) {
    .ui-appbar {
        padding-top: env(safe-area-inset-top, 0px);
    }

    .ui-footer {
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }
}

/* The side insets stay unconditional: they only bite in landscape, where a
   notch genuinely overlaps the page in a browser tab as well, and max() means
   they can never make the gutter smaller than the design's own padding. */
.ui-appbar__inner,
.ui-footer__inner {
    padding-left: max(var(--ui-s-4), env(safe-area-inset-left, 0px));
    padding-right: max(var(--ui-s-4), env(safe-area-inset-right, 0px));
}

/* Install / Add-to-Home-Screen prompt, injected by components/pwa.js. */
.pibera-install-bar {
    position: fixed;
    z-index: 200;
    left: max(12px, env(safe-area-inset-left, 0px));
    right: max(12px, env(safe-area-inset-right, 0px));
    bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    margin-inline: auto;
    max-width: 520px;
    display: flex;
    align-items: center;
    gap: var(--ui-s-3);
    padding: 12px 14px;
    background: var(--ui-surface-1);
    color: var(--ui-text);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-md, 12px);
    box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
    transform: translateY(140%);
    transition: transform .32s ease;
}

.pibera-install-bar.is-in { transform: translateY(0); }

.pibera-install-bar img { border-radius: 8px; flex: none; }

.pibera-install-bar__text {
    flex: 1;
    font-size: .9rem;
    line-height: 1.35;
}

.pibera-install-bar__go {
    flex: none;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    border: 0;
    border-radius: 8px;
    padding: 9px 16px;
    background: var(--ui-accent, #026f39);
    color: #fff;
}

.pibera-install-bar__close {
    flex: none;
    font-size: 1.35rem;
    line-height: 1;
    cursor: pointer;
    border: 0;
    background: transparent;
    color: var(--ui-text-muted, #64748b);
    padding: 4px 6px;
}

@media (prefers-reduced-motion: reduce) {
    .pibera-install-bar { transition: none; }
}

/* ─── Page top spacing on phones ─────────────────────────────────────
   Below 900px there is no top nav (the bottom bar carries navigation),
   so the page starts directly under the status bar. The desktop
   24px page inset then reads as a band of dead space above the first line
   of every screen. Tighten it on phones only; the desktop rhythm is
   unchanged.

   (The title's own top margin used to be zeroed here as well. It is 0
   everywhere now -- see .ui-page-head__title -- so there is nothing left
   to correct.) */
@media (max-width: 899px) {
    body[data-ui="v2"] .ui-page { padding-block-start: var(--ui-s-3); }
}

/* ─── More sheet: text size ───────────────────────────────────────────
   Four steps in a row, each label drawn at the size it sets, so the choice is
   made by looking rather than by guessing what "Larger" means.

   The glyph sizes are the literal rem values of the scale steps, NOT the t-*
   roles: these previews must keep their relative sizes no matter which step is
   currently active, and a role would scale with the root along with everything
   else, flattening the row into four identical letters. */
.ui-sheet__textsize {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--ui-s-2);
    margin-bottom: var(--ui-s-3);
}
.ui-sheet__textsize-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    gap: 2px;
    /* Comfortably past the 34px floor in UI-STANDARD.md §3: this is a control
       people with unsteady hands reach for by definition. */
    min-height: 56px;
    padding: var(--ui-s-2) var(--ui-s-1);
    border: 1px solid var(--ui-border);
    border-radius: var(--ui-r-lg);
    background: var(--ui-surface-2);
    color: var(--ui-text);
    cursor: pointer;
    transition: background .16s ease, border-color .16s ease;
}
.ui-sheet__textsize-btn:hover { background: var(--ui-surface-3, var(--ui-surface-2)); }
.ui-sheet__textsize-btn[aria-pressed="true"] {
    border-color: var(--ui-accent);
    background: var(--ui-accent-soft, var(--ui-surface-2));
}
/* State is never carried by colour alone (UI-STANDARD.md §6) — the chosen
   step also gains weight, so it reads without colour vision. */
.ui-sheet__textsize-btn[aria-pressed="true"] .ui-sheet__textsize-name { font-weight: 800; }

.ui-sheet__textsize-glyph { line-height: 1; font-weight: 800; }
.ui-sheet__textsize-glyph[data-step="default"] { font-size: 0.875rem; }
.ui-sheet__textsize-glyph[data-step="large"]   { font-size: 1rem; }
.ui-sheet__textsize-glyph[data-step="larger"]  { font-size: 1.125rem; }
.ui-sheet__textsize-glyph[data-step="largest"] { font-size: 1.3125rem; }

.ui-sheet__textsize-name {
    font-size: var(--t-meta);
    font-weight: 600;
    color: var(--ui-text-muted, inherit);
}

@media (prefers-reduced-motion: reduce) {
    .ui-sheet__textsize-btn { transition: none; }
}

/* ─── Browse bar ─────────────────────────────────────────────────────── */
/*
 * The section header + search + Filter row that sits above a catalogue.
 * It was written twice — once in Tailwind on /quests, once as .tool-toolbar
 * on /tools — and the two drifted apart within a week. One component now,
 * tokenised so it follows the theme, and used by both.
 */

.ui-browsebar {
    display: flex;
    flex-direction: column;
    gap: var(--ui-s-3);
    padding-top: var(--ui-s-3);
    border-top: 1px solid var(--ui-border);
}
.ui-browsebar > * { min-width: 0; }

.ui-browsebar__title {
    margin: 0;
    font-size: var(--t-body-sm);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ui-text-muted);
}

.ui-browsebar__controls {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
    width: 100%;
}
.ui-browsebar__controls > * { min-width: 0; }

/* The search box owns its icon and its clear button, so the input keeps
   room for both rather than running text underneath them. */
.ui-browsebar__search {
    position: relative;
    flex: 1 1 auto;
}
.ui-browsebar__search .ui-input {
    width: 100%;
    padding-left: 36px;
    padding-right: 32px;
}
.ui-browsebar__search-icon,
.ui-browsebar__clear {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    color: var(--ui-text-faint);
    pointer-events: none;
}
.ui-browsebar__search-icon { left: 12px; width: 16px; height: 16px; }
.ui-browsebar__clear {
    right: 8px;
    width: 26px; height: 26px;
    display: grid; place-items: center;
    border: 0; background: none; padding: 0;
    border-radius: var(--ui-r-sm);
    cursor: pointer;
    pointer-events: auto;
}
.ui-browsebar__clear:hover { color: var(--ui-text); background: var(--ui-surface-3); }
.ui-browsebar__clear svg { width: 14px; height: 14px; }

.ui-browsebar__filter {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 9px 13px;
    border-radius: var(--ui-r-md);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-1);
    color: var(--ui-text-secondary);
    font-family: inherit;
    font-size: var(--t-body-sm);
    font-weight: 700;
    line-height: 1.2;
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease), border-color var(--ui-fast) var(--ui-ease);
}
.ui-browsebar__filter:hover { background: var(--ui-surface-3); border-color: var(--ui-border-strong, var(--ui-border)); }
.ui-browsebar__filter svg { width: 14px; height: 14px; }
.ui-browsebar__count {
    padding: 2px 7px;
    border-radius: var(--ui-r-full);
    background: var(--ui-accent);
    color: var(--ui-accent-contrast, #fff);
    font-size: var(--t-meta);
    font-weight: 800;
    line-height: 1.2;
}

@media (min-width: 640px) {
    .ui-browsebar {
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
    }
    .ui-browsebar__controls { width: auto; }
    .ui-browsebar__search { width: 18rem; flex: 0 0 auto; }
}

/* ─── Rail section + category tiles ──────────────────────────────────── */
/*
 * "Explore by Category": a heading with a Show-all link and two scroll
 * arrows, over a snapping horizontal rail of tiles. Written first in
 * Tailwind on /quests and again as .tool-section on /tools; one component
 * now, so a change to the tile lands on both pages at once.
 */

.ui-railsection { display: flex; flex-direction: column; gap: var(--ui-s-3); }
.ui-railsection > * { min-width: 0; }

.ui-railsection__head {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--ui-s-2);
}
.ui-railsection__title {
    margin: 0;
    display: inline-flex; align-items: center; gap: var(--ui-s-2);
    font-size: var(--t-body-sm);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ui-text-muted);
}
.ui-railsection__tools { display: inline-flex; align-items: center; gap: var(--ui-s-2); }
.ui-railsection__arrows { display: inline-flex; align-items: center; gap: 4px; }
.ui-railarrow {
    width: 28px; height: 28px;
    display: grid; place-items: center;
    border-radius: var(--ui-r-sm);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-1);
    color: var(--ui-text-secondary);
    cursor: pointer;
    padding: 0;
    transition: background var(--ui-fast) var(--ui-ease);
}
.ui-railarrow:hover { background: var(--ui-surface-3); color: var(--ui-text); }
.ui-railarrow svg { width: 14px; height: 14px; }

.ui-rail {
    display: flex;
    gap: var(--ui-s-3);
    overflow-x: auto;
    scroll-snap-type: x proximity;
    padding-bottom: 4px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.ui-rail::-webkit-scrollbar { display: none; }
.ui-rail > * { scroll-snap-align: start; flex: 0 0 auto; }

/* One category tile: icon block, name, count. */
.ui-cattile {
    display: flex; align-items: center; gap: var(--ui-s-3);
    min-width: 150px; max-width: 220px;
    padding: var(--ui-s-3);
    border-radius: var(--ui-r-lg);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-1);
    color: var(--ui-text);
    text-align: left;
    cursor: pointer;
    user-select: none;
    font-family: inherit;
    transition: border-color var(--ui-fast) var(--ui-ease),
                transform var(--ui-fast) var(--ui-ease),
                background var(--ui-fast) var(--ui-ease);
}
.ui-cattile:hover { border-color: var(--ui-border-strong, var(--ui-text-faint)); }
.ui-cattile__icon {
    flex: 0 0 auto;
    width: 40px; height: 40px;
    display: grid; place-items: center;
    border-radius: var(--ui-r-md);
    background: var(--ui-surface-3);
    font-size: 1.25rem;
}
.ui-cattile__body { min-width: 0; display: block; }
.ui-cattile__name {
    display: block;
    margin: 0;
    font-size: var(--t-body-sm);
    font-weight: 700;
    line-height: 1.25;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ui-cattile__count {
    display: block;
    margin: 2px 0 0;
    font-size: var(--t-meta);
    font-weight: 600;
    color: var(--ui-text-faint);
}
.ui-cattile--on {
    background: var(--ui-text);
    border-color: var(--ui-text);
    color: var(--ui-surface-1);
    transform: translateY(-2px);
    box-shadow: var(--ui-shadow-md, 0 4px 14px rgba(0, 0, 0, 0.12));
}
.ui-cattile--on .ui-cattile__icon { background: rgba(255, 255, 255, 0.2); }
.ui-cattile--on .ui-cattile__count { color: inherit; opacity: 0.72; }

@media (prefers-reduced-motion: reduce) {
    .ui-cattile, .ui-railarrow { transition: none; }
    .ui-cattile--on { transform: none; }
}

/* ─── Filter sheet ───────────────────────────────────────────────────── */
/*
 * The body of a .ui-sheet-modal used as a filter panel: a head with a title
 * and a close button, fields of chips, and a two-button foot. Quests and
 * tools share it, so "Filter" behaves the same wherever it is tapped.
 */

/*
 * THE HEAD AND FOOT STAY; THE BODY IS WHAT SCROLLS.
 *
 * The panel is a flex column capped at 88dvh with `overflow: hidden`, so a
 * body taller than the panel does not scroll — it pushes the foot out of
 * the bottom of the panel, where `overflow: hidden` then clips it away
 * entirely. Nothing is off-screen to scroll TO, so the sheet's buttons are
 * simply gone.
 *
 * It went unnoticed while every sheet here was a short list of chips. It
 * bit the moment one had a Save in its foot and a list of sounds in its
 * body: the sheet could be read and changed but not saved.
 *
 * `min-height: 0` is the load-bearing half. A flex item's default minimum
 * is its content, which is what lets the body refuse to shrink and shove
 * the foot out in the first place; `overflow-y: auto` alone does nothing
 * until the item is allowed to be smaller than what is in it.
 */
.ui-sheet-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--ui-s-3);
    padding: var(--ui-s-4) var(--ui-s-5);
    border-bottom: 1px solid var(--ui-border);
    flex: none;
}
.ui-sheet-head__title { margin: 0; font-size: var(--t-title); font-weight: 800; color: var(--ui-text); }
.ui-sheet-close {
    width: 32px; height: 32px;
    display: grid; place-items: center;
    border: 0; background: none; padding: 0;
    border-radius: var(--ui-r-sm);
    color: var(--ui-text-faint);
    cursor: pointer;
}
.ui-sheet-close:hover { background: var(--ui-surface-3); color: var(--ui-text); }

.ui-sheet-body {
    display: flex; flex-direction: column; gap: var(--ui-s-5);
    padding: var(--ui-s-5);
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* The page behind a sheet does not scroll on when the list inside it
       reaches its end. */
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}
.ui-sheet-field { display: flex; flex-direction: column; gap: var(--ui-s-2); }
.ui-sheet-field__label {
    font-size: var(--t-meta);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ui-text-muted);
}
.ui-sheet-field__options { display: flex; flex-wrap: wrap; gap: var(--ui-s-2); }

.ui-sheet-foot {
    display: flex; align-items: center; gap: var(--ui-s-2);
    padding: var(--ui-s-4) var(--ui-s-5);
    border-top: 1px solid var(--ui-border);
    flex: none;
    /* Opaque, in the panel's own colour: the body now scrolls under it. */
    background: var(--ui-surface-2);
}
.ui-sheet-foot > .ui-btn { flex: 1 1 0; min-width: 0; }

.ui-filterchip {
    padding: 7px 13px;
    border-radius: var(--ui-r-md);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-1);
    color: var(--ui-text-secondary);
    font-family: inherit;
    font-size: var(--t-body-sm);
    font-weight: 700;
    line-height: 1.2;
    cursor: pointer;
    transition: background var(--ui-fast) var(--ui-ease), border-color var(--ui-fast) var(--ui-ease);
}
.ui-filterchip:hover { border-color: var(--ui-text-faint); }
.ui-filterchip--on {
    background: var(--ui-text);
    border-color: var(--ui-text);
    color: var(--ui-surface-1);
}

/* ─── The ⓘ tooltip (components/ui-tip.js) ────────────────────────────
 *
 * The trigger is .ui-info, already defined above — this adds only the bubble
 * and the wrapper, so the circle looks the same wherever it explains
 * something. The bubble is moved to <body> while open (a card's
 * backdrop-filter would otherwise become its containing block and position it
 * against the card), which is why it is styled without reference to anything
 * around it.
 */
.ui-tip { display: inline-flex; flex: none; }
.ui-tip__trigger { width: 26px; height: 26px; font-size: 0.8125rem; }

.ui-tip__bubble {
    /* Fixed with measured coordinates the moment it opens — see place(). */
    position: fixed;
    z-index: 340;                 /* above menus (320) and modals (300) */
    max-width: min(20rem, calc(100vw - 24px));
    padding: 10px 12px;
    border-radius: 12px;
    background: var(--ui-ink, #0f172a);
    color: var(--ui-ink-text, #f8fafc);
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.5;
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.06), 0 12px 28px -6px rgb(0 0 0 / 0.28);
    animation: ui-tip-in var(--ui-fast) var(--ui-ease);
}
.ui-tip__bubble.hidden { display: none; }

@keyframes ui-tip-in {
    from { opacity: 0; transform: translateY(-3px); }
    to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
    .ui-tip__bubble { animation: none; }
}

/* ─── Home spotlight (services/home-spotlight.js) ─────────────────────
 *
 * The band at the top of a signed-in home page. Five looks, chosen by an
 * admin from a fixed list — the variant name is written into the class here,
 * so a value that is not one of these renders an unstyled band. The service
 * validates on write and again on read for exactly that reason.
 *
 * Four of the five are tone-soft surfaces built from tokens, so they follow
 * the theme without a dark override. `--midnight` is the exception: it is
 * deliberately dark in BOTH themes, because its job is to be the one thing on
 * the page that is not the page. Its text colours are therefore fixed rather
 * than tokenised — a token would flip them to dark ink on a dark gradient the
 * moment someone switched theme.
 */
.ui-spotlight {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    padding: var(--ui-s-6);
    border-radius: var(--ui-r-xl);
    border: 1px solid var(--ui-border);
    background: var(--ui-surface-1);
}
.ui-spotlight > * { min-width: 0; }

.ui-spotlight__inner {
    position: relative;
    z-index: 1;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-5);
}
/* The copy column takes the space; the button keeps its own width. Both are
   allowed to shrink to nothing, so a long headline wraps instead of pushing
   the button off a 320px screen. */
.ui-spotlight__copy { flex: 1 1 20rem; min-width: 0; }
.ui-spotlight__actions { flex: 0 0 auto; }

.ui-spotlight__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-s-2);
    margin-bottom: var(--ui-s-3);
    padding: 4px 11px;
    border-radius: var(--ui-r-full);
    background: var(--ui-accent-soft);
    border: 1px solid var(--ui-accent-line);
    color: var(--ui-accent-text);
    font-size: var(--t-meta);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
.ui-spotlight__headline {
    margin: 0;
    font-size: var(--t-h3);
    font-weight: 800;
    line-height: 1.35;
    color: var(--ui-text);
    overflow-wrap: anywhere;
}
.ui-spotlight__body {
    margin: var(--ui-s-2) 0 0;
    font-size: var(--t-body-sm);
    line-height: 1.6;
    color: var(--ui-text-secondary);
    overflow-wrap: anywhere;
}

/* The decorative orbs. pointer-events:none so they never eat a click meant
   for the button underneath them. */
.ui-spotlight__orb {
    position: absolute;
    width: 18rem;
    height: 18rem;
    max-width: 100%;
    border-radius: var(--ui-r-full);
    filter: blur(64px);
    opacity: 0.55;
    pointer-events: none;
    z-index: 0;
}
/* Gold above, plum below: the jewel's own light, not two blue lamps. */
.ui-spotlight__orb--a { top: -6rem; right: -6rem; background: rgba(217, 164, 65, 0.30); }
.ui-spotlight__orb--b { bottom: -6rem; left: -6rem; background: rgba(99, 102, 241, 0.32); }

/* The midnight band is the app's other dark panel, so it is the jewel too —
   the same plum the crest is struck on, lit from above the way the crest is.
   Left as navy it was the one cold object on a warm page, and it sat at the
   top of the home screen where it was the first thing anybody saw. */
.ui-spotlight--midnight {
    background: radial-gradient(120% 140% at 50% -20%, #3a2c66 0%, #241a44 38%, #120e24 100%);
    border-color: rgba(244, 239, 228, 0.14);
}
.ui-spotlight--midnight .ui-spotlight__headline { color: #f4efe4; }
.ui-spotlight--midnight .ui-spotlight__body { color: #a79cc4; }
.ui-spotlight--midnight .ui-spotlight__eyebrow {
    background: rgba(59, 130, 246, 0.20);
    border-color: rgba(96, 165, 250, 0.35);
    color: #93c5fd;
}

/* The soft variants. Each is one tinted surface plus a matching border; the
   orbs are hidden on them because a blurred blob on a pale card reads as a
   rendering artefact rather than a decoration. */
.ui-spotlight--sky,
.ui-spotlight--amber,
.ui-spotlight--emerald,
.ui-spotlight--plain { color: var(--ui-text); }
.ui-spotlight--sky     { background: var(--ui-accent-soft);  border-color: var(--ui-accent-line); }
.ui-spotlight--amber   { background: var(--ui-warning-soft);  border-color: var(--ui-warning); }
.ui-spotlight--emerald { background: var(--ui-success-soft);  border-color: var(--ui-success); }
.ui-spotlight--plain   { background: var(--ui-surface-1);     border-color: var(--ui-border); }

.ui-spotlight--amber .ui-spotlight__eyebrow {
    background: var(--ui-warning-soft);
    border-color: var(--ui-warning);
    color: var(--ui-warning-text);
}
.ui-spotlight--emerald .ui-spotlight__eyebrow {
    background: var(--ui-success-soft);
    border-color: var(--ui-success);
    color: var(--ui-success-text);
}

/* ─── Spotlight wireframe loader ──────────────────────────────────────
 *
 * The band used to be `hidden` until its fetch returned, so the whole "For
 * you" column started at the top of the page and jumped down a beat later.
 * A wireframe of the band's real shape is painted instead: same padding,
 * same border radius, same three stacked lines and a button block, so the
 * space the band will occupy is already reserved on first paint and nothing
 * below it moves. When there is no spotlight today the loader is removed and
 * the section goes back to being hidden — the empty state is still nothing,
 * not an empty rectangle.
 */
.ui-spotlight--loading {
    background: var(--ui-surface-1);
    border-color: var(--ui-border);
}
.ui-spotlight--loading .ui-spotlight__orb { display: none; }

.ui-skel {
    display: block;
    border-radius: var(--ui-r-md, 8px);
    background: linear-gradient(
        90deg,
        var(--ui-surface-2) 0%,
        var(--ui-border) 50%,
        var(--ui-surface-2) 100%
    );
    background-size: 200% 100%;
    animation: ui-skel-shimmer 1.4s ease-in-out infinite;
}
/* A card-shaped wireframe: the stacked lines need their own rhythm because
   ui-card has no flow spacing of its own. */
.ui-skel-card {
    display: flex;
    flex-direction: column;
    gap: var(--ui-s-3);
    min-height: 8.5rem;
}
.ui-skel-card .ui-skel--line { margin-top: 0; }

.ui-skel--eyebrow  { width: 6.5rem;  height: 1.35rem; border-radius: var(--ui-r-full); margin-bottom: var(--ui-s-3); }
.ui-skel--headline { width: min(28rem, 85%); height: 1.35rem; }
.ui-skel--line     { width: min(34rem, 95%); height: 0.9rem; margin-top: var(--ui-s-3); }
.ui-skel--line-short { width: min(18rem, 60%); }
.ui-skel--btn      { width: 8.5rem; height: 2.6rem; border-radius: var(--ui-r-full); }

@keyframes ui-skel-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .ui-skel { animation: none; }
}

.ui-spotlight--sky .ui-spotlight__orb,
.ui-spotlight--amber .ui-spotlight__orb,
.ui-spotlight--emerald .ui-spotlight__orb,
.ui-spotlight--plain .ui-spotlight__orb { display: none; }

/* ─── Daily Family Spark (services/family-spark.js) ───────────────────
 *
 * One prompt a day. Built as an accent-soft panel rather than a plain card so
 * it is visibly a different kind of thing from the quest and tool cards it
 * sits among — it asks for a conversation, not a click.
 */
.ui-spark {
    padding: var(--ui-s-5);
    border-radius: var(--ui-r-xl);
    border: 1px solid var(--ui-accent-line);
    background: var(--ui-accent-soft);
}
.ui-spark > * { min-width: 0; }

.ui-spark__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-3);
    margin-bottom: var(--ui-s-3);
}
.ui-spark__title {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-s-2);
    font-size: var(--t-meta);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--ui-accent-text);
}
.ui-spark__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    flex: 0 0 auto;
    border-radius: var(--ui-r-full);
    background: var(--ui-accent);
    color: var(--ui-on-accent);
    font-size: 0.8rem;
}
.ui-spark__tag {
    padding: 3px 10px;
    border-radius: var(--ui-r-full);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-accent-line);
    color: var(--ui-accent-text);
    font-size: var(--t-meta);
    font-weight: 800;
}
.ui-spark__prompt {
    margin: 0;
    font-size: var(--t-title);
    font-weight: 800;
    line-height: 1.45;
    color: var(--ui-text);
    overflow-wrap: anywhere;
}
.ui-spark__note {
    margin: var(--ui-s-2) 0 0;
    font-size: var(--t-body-sm);
    line-height: 1.6;
    color: var(--ui-text-secondary);
    overflow-wrap: anywhere;
}
.ui-spark__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-3);
    margin-top: var(--ui-s-4);
}
.ui-spark__points {
    font-size: var(--t-body-sm);
    font-weight: 800;
    color: var(--ui-accent-text);
}
/* Logged is a state, not a disabled button: the label changes to say what
   happened, and the control stops being pressable. */
.ui-spark--logged { border-color: var(--ui-success); background: var(--ui-success-soft); }
.ui-spark--logged .ui-spark__points { color: var(--ui-success-text); }

/* ─── The For You rail ────────────────────────────────────────────────
 *
 * The tab strip scrolls sideways on a narrow screen rather than wrapping to
 * two rows, which is why `.ui-shell > * { min-width: 0 }` above is load
 * bearing: without it this scroller sets the width of the whole column.
 */
.ui-foryou__tabs {
    display: flex;
    align-items: center;
    gap: var(--ui-s-2);
    overflow-x: auto;
    scrollbar-width: none;
    padding: 2px 0;
}
.ui-foryou__tabs::-webkit-scrollbar { display: none; }
.ui-foryou__tabs > * { flex: 0 0 auto; }

.ui-foryou__count {
    margin-left: var(--ui-s-2);
    padding: 1px 7px;
    border-radius: var(--ui-r-full);
    background: var(--ui-accent-soft);
    color: var(--ui-accent-text);
    font-size: var(--t-meta);
    font-weight: 800;
}
.ui-filterchip--on .ui-foryou__count {
    background: rgba(255, 255, 255, 0.22);
    color: inherit;
}

/* A module heading: smaller and quieter than a section head, because these
   are dividers inside one section rather than sections of their own. */
.ui-modulehead {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ui-s-3);
    margin-bottom: var(--ui-s-3);
}
.ui-modulehead__title {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-s-2);
    margin: 0;
    font-size: var(--t-meta);
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--ui-text-muted);
}

/* ─── The quest card (components/quest-card.js) ───────────────────────
 *
 * The card is one clickable target: the title link is stretched over the
 * whole article rather than an onclick being put on the article itself, so
 * the card keeps exactly ONE real link — keyboard-reachable, announced once
 * rather than three times, and openable in a new tab like any other link.
 *
 * These two rules used to live inline in modules/quests.html, which was fine
 * while that page was the only one drawing the card. The home page now draws
 * the same card, and without them its cards are clickable only on the title
 * text — the kind of regression that looks like nothing and feels broken.
 */
.quest-card__link::after { content: ""; position: absolute; inset: 0; z-index: 1; }
/* Anything interactive in its own right has to sit above that, or the
   stretched link eats the click. The save bookmark is the only one today. */
.quest-card__above { position: relative; z-index: 2; }

/* ════════════════════════════════════════════════════════════════════════
   THE CREST — /profile

   The profile is the HOUSEHOLD's page. It opens on a sigil, a level and the
   faces you are building it with; everything else is a ledger beneath.

   The crest panel is always the same dark jewel, in either theme, because it
   is an object rather than a surface — an emblem that repaints itself is not
   an emblem. What changes around it is the ground: warm paper by day, deep
   plum at night. Those grounds are declared here rather than taken from the
   app tokens, because this page is a room of its own and the rest of Pibera
   should not drift into it by accident.

   Every size below is in rem and nothing is under 0.75rem — UI-STANDARD §7.
   The gold is spent on exactly two things: the household's own identity, and
   anything waiting for a decision. Nothing else may use it.
   ════════════════════════════════════════════════════════════════════════ */

/*
 * THE TOKENS LIVE ON THE PAGE, NOT ON THE ROOT ELEMENT.
 *
 * They were declared on `.crest` alone. Sheets are appended to <body> so they
 * can sit above everything — which put them OUTSIDE that subtree, where every
 * --cr-* resolved to nothing: a transparent panel, no radius, the app's text
 * colour on the app's background. Opening a child's settings looked like the
 * page had come apart, and nothing threw, so no error ever pointed at it.
 *
 * Declared on the body as well, anything in the page inherits them wherever
 * it is mounted.
 */
body[data-module="profile"],
/*
 * And for the cabinet. /badges is the crest's trophy shelf opened full
 * size, so it takes the same palette rather than declaring a second one:
 * two palettes for one object is how the profile's medals and the badge
 * wall's medals came to be different colours in the first place.
 */
body[data-module="badges"],
/*
 * The crest's palette is declared for the profile AND for the You drawer.
 *
 * The drawer opens from your own face in the tab bar and leads into this hub,
 * so it belongs to the crest's world rather than to whatever page it happens
 * to be sliding over. It was inheriting the app's --ui-* tokens, which made it
 * purple-on-white beside a profile of warm paper and gold — the same control,
 * two skins, depending on which side of the tap you were on.
 *
 * Tokens only. .crest also paints a full-height page background below, which a
 * sheet must not do.
 */
.crest,
.ui-sheet__panel,
.ui-sheet-modal--crest .ui-sheet-modal__panel {
    /* The jewel — constant in both themes. */
    --cr-deep: #120e24;
    --cr-plum: #241a44;
    --cr-gold: #d9a441;
    --cr-gold-2: #f0d08a;
    --cr-on-jewel: #f4efe4;
    --cr-on-jewel-mute: #a79cc4;
    --cr-jewel-line: rgba(244, 239, 228, 0.14);

    /* The ground — day. */
    --cr-bg: #f6f1e7;
    --cr-surface: #fffdf8;
    --cr-surface-2: #f1e9da;
    --cr-ink: #241c33;
    --cr-ink-2: #554b6b;
    --cr-ink-3: #6f6584;
    --cr-line: rgba(36, 28, 51, 0.13);
    --cr-line-soft: rgba(36, 28, 51, 0.07);
    --cr-accent: #7d5110;
    --cr-accent-soft: rgba(217, 164, 65, 0.16);
    --cr-accent-line: rgba(217, 164, 65, 0.5);
    --cr-danger: #9c2b2b;
    --cr-danger-soft: rgba(156, 43, 43, 0.08);
    --cr-ok: #2f6b4f;
    --cr-ok-soft: rgba(47, 107, 79, 0.12);
    --cr-shadow: 0 2px 6px rgba(36, 28, 51, 0.05), 0 14px 34px -18px rgba(36, 28, 51, 0.28);
    --cr-shadow-lg: 0 20px 48px -20px rgba(36, 28, 51, 0.45);

    --cr-r: 14px;
    --cr-r-lg: 20px;
    --cr-r-xl: 26px;
}

/*
 * The PAGE paints itself full height. The drawer must not: it is a sheet that
 * hugs its content and rests on the bottom edge, and min-height: 100vh made it
 * fill the screen with a field of empty paper under the last row.
 */
.crest {
    background: var(--cr-bg);
    color: var(--cr-ink);
    min-height: 100vh;
}

html[data-ui-theme="dark"] body[data-module="profile"],
html[data-ui-theme="dark"] body[data-module="badges"],
html[data-ui-theme="dark"] .crest,
html[data-ui-theme="dark"] .ui-sheet__panel,
html[data-ui-theme="dark"] .ui-sheet-modal--crest .ui-sheet-modal__panel {
    --cr-bg: #161129;
    --cr-surface: #201839;
    --cr-surface-2: #2a2049;
    --cr-ink: #f4efe4;
    --cr-ink-2: #c9bfe0;
    --cr-ink-3: #a79cc4;
    --cr-line: rgba(244, 239, 228, 0.13);
    --cr-line-soft: rgba(244, 239, 228, 0.07);
    --cr-accent: #e7c06a;
    --cr-accent-soft: rgba(217, 164, 65, 0.14);
    --cr-accent-line: rgba(217, 164, 65, 0.4);
    --cr-danger: #e88f8f;
    --cr-danger-soft: rgba(232, 143, 143, 0.12);
    --cr-ok: #7fc8a3;
    --cr-ok-soft: rgba(127, 200, 163, 0.13);
    --cr-shadow: 0 2px 6px rgba(0, 0, 0, 0.4), 0 14px 34px -18px rgba(0, 0, 0, 0.7);
    --cr-shadow-lg: 0 20px 48px -20px rgba(0, 0, 0, 0.8);
}

/* The page owns the whole viewport, not just its content box. */
body[data-module="profile"],
body[data-module="badges"] { background: var(--cr-bg); color: var(--cr-ink); }

.crest__shell { width: 100%; max-width: 1080px; margin-inline: auto; }
.crest__shell > * { min-width: 0; }

/* ── Atoms ──────────────────────────────────────────────────────────── */
.cr-serif { font-family: Fraunces, "Plus Jakarta Sans", Georgia, serif; }

.cr-eyebrow {
    margin: 0;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--cr-ink-3);
}

.cr-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    border-radius: 999px;
    border: 1px solid var(--cr-line);
    background: var(--cr-surface);
    color: var(--cr-ink);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    padding: 10px 18px;
    min-height: 40px;
    cursor: pointer;
    text-decoration: none;
}
.cr-btn--gold {
    background: linear-gradient(160deg, var(--cr-gold-2), var(--cr-gold));
    border-color: transparent;
    color: #2a2012;
    font-weight: 700;
}
.cr-btn--ghost { background: transparent; }
.cr-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.cr-btn--danger { color: var(--cr-danger); background: var(--cr-danger-soft); border-color: transparent; }
.cr-btn--sm { font-size: 0.8125rem; padding: 7px 14px; min-height: 34px; }
.cr-btn--block { width: 100%; }
.cr-btn:focus-visible,
.cr-row:focus-visible,
.cr-led:focus-visible,
.cr-face:focus-visible { outline: 2px solid var(--cr-gold); outline-offset: 2px; }

.cr-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 11px;
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 700;
    background: var(--cr-accent-soft);
    color: var(--cr-accent);
    border: 1px solid var(--cr-accent-line);
}
.cr-chip--ok { background: var(--cr-ok-soft); color: var(--cr-ok); border-color: transparent; }
.cr-chip--off { background: var(--cr-surface-2); color: var(--cr-ink-3); border-color: transparent; }

/*
 * THE ROW — the page's workhorse.
 *
 * The label is a block and the description is a block. As inline spans they
 * run together on one line, which is exactly how the previous profile shipped
 * its settings list. Here they are <p> and <small> and cannot.
 */
.cr-row {
    display: flex;
    align-items: center;
    gap: 13px;
    width: 100%;
    text-align: left;
    padding: 13px 14px;
    border-radius: var(--cr-r);
    background: var(--cr-surface);
    border: 1px solid var(--cr-line-soft);
    text-decoration: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
}
.cr-row:hover { border-color: var(--cr-accent-line); background: var(--cr-accent-soft); }
.cr-row__b { flex: 1 1 auto; min-width: 0; }
.cr-row p { margin: 0; font-size: 0.9375rem; font-weight: 600; letter-spacing: -0.005em; }
.cr-row small {
    display: block;
    margin-top: 2px;
    font-size: 0.8125rem;
    color: var(--cr-ink-3);
    line-height: 1.45;
    overflow-wrap: anywhere;
}
.cr-row__n {
    font-family: Fraunces, Georgia, serif;
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--cr-accent);
    font-variant-numeric: tabular-nums;
    flex: none;
}
.cr-row__car { flex: none; color: var(--cr-ink-3); font-size: 0.9375rem; }
/* A row that is only telling you something should not look pressable. */
.cr-row--static { cursor: default; }
.cr-row--static:hover { background: var(--cr-surface); border-color: var(--cr-line-soft); }

/*
 * A toggle row.
 *
 * This was `.cr-label .cr-row` on one element — two display rules fighting,
 * and `.cr-label`'s `block` won by source order, so the checkbox and its text
 * stacked instead of sitting in a line. One class, one display.
 */
.cr-toggle {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    margin-top: 10px;
    padding: 13px 14px;
    border-radius: var(--cr-r);
    background: var(--cr-surface);
    border: 1px solid var(--cr-line-soft);
    cursor: pointer;
}
.cr-toggle > * { min-width: 0; }
.cr-toggle input { flex: none; width: 20px; height: 20px; accent-color: var(--cr-gold); }
.cr-toggle p { margin: 0; font-size: 0.9375rem; font-weight: 600; }
.cr-toggle small { display: block; margin-top: 2px; font-size: 0.8125rem; color: var(--cr-ink-3); line-height: 1.45; }

.cr-stack { display: flex; flex-direction: column; gap: 9px; }

/*
 * A SWITCH, not a pair of On/Off buttons.
 *
 * On and Off side by side are two controls for one fact, and neither of them
 * looks like the answer until you read which is pressed. A switch IS the
 * answer: its position is the state, and moving it is the whole gesture.
 *
 * Same track geometry as .ui-toggle so the two systems agree on what a switch
 * is; the crest's own tokens colour it.
 */
.cr-switch {
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 13px 14px;
    border-radius: var(--cr-r);
    background: var(--cr-surface);
    border: 1px solid var(--cr-line-soft);
    cursor: pointer;
    user-select: none;
}
.cr-switch > * { min-width: 0; }
.cr-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
.cr-switch__track {
    position: relative;
    flex: none;
    order: 2;
    margin-left: auto;
    width: 44px;
    height: 26px;
    border-radius: 999px;
    background: var(--cr-surface-2);
    border: 1px solid var(--cr-line);
    transition: background .16s ease, border-color .16s ease;
}
.cr-switch__track::after {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: 999px;
    background: #fffdf8;
    box-shadow: 0 1px 3px rgb(0 0 0 / .3);
    transition: transform .16s ease;
}
/*
 * `~`, NOT `+`.
 *
 * The track is the LAST child and is pulled to the right edge with `order`,
 * so .cr-switch__b sits between it and the input. An adjacent-sibling rule
 * therefore never matched: the checkbox flipped, the track never repainted,
 * and a switch that changes state without showing it is indistinguishable
 * from a broken one. Every rule that styles the track from the input's state
 * has to be a general-sibling rule for as long as the track is not adjacent.
 */
.cr-switch input:checked ~ .cr-switch__track {
    background: linear-gradient(160deg, var(--cr-gold-2), var(--cr-gold));
    border-color: transparent;
}
.cr-switch input:checked ~ .cr-switch__track::after { transform: translateX(18px); }
.cr-switch input:focus-visible ~ .cr-switch__track { outline: 2px solid var(--cr-gold); outline-offset: 2px; }
.cr-switch__b { flex: 1 1 auto; }
.cr-switch__b p { margin: 0; font-size: 0.9375rem; font-weight: 600; }
.cr-switch__b small { display: block; margin-top: 2px; font-size: 0.8125rem; color: var(--cr-ink-3); line-height: 1.45; }
/* A category that cannot be turned on is shown, greyed and explained rather
   than hidden: an absence you can see reads as a decision, one you cannot
   reads as an oversight. */
.cr-switch--fixed { cursor: not-allowed; opacity: 0.62; }

/* The pick tiles sit in sheets too, where the column is narrower than a page's
   — two across at 520px, one on a phone. */
.cr-sheet__body .cr-picks { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
/* ── Pibera Plus: the plan screen and its code sheet ──────────────────
 *
 * Scoped to .cr-plan (the screen) and .cr-sheet--plan (the code sheet) so the
 * rest of the profile is untouched. Everything a parent reads here is at the
 * 14px floor; only the offer badge is meta (12px). One spacing rhythm:
 * 32 between sections, 16 between blocks, 12 inside a group, 8 for a hint.
 */
.cr-plan .cr-sect { margin: 32px 0 14px; }
.cr-plan .cr-sect p,
.cr-plan .cr-hint,
.cr-sheet--plan .cr-hint { font-size: 0.875rem; line-height: 1.55; }
.cr-plan .cr-hint { margin-top: 10px; }
.cr-plan .cr-note p { font-size: 0.875rem; }
.cr-plan .cr-row small,
.cr-plan .cr-pick__d,
.cr-sheet--plan .cr-pick__d { font-size: 0.875rem; line-height: 1.5; margin-top: 4px; }
.cr-plan .cr-pick__w,
.cr-sheet--plan .cr-pick__w { font-size: 0.75rem; margin-top: 10px; }
.cr-plan .cr-pick,
.cr-sheet--plan .cr-pick { padding: 16px; gap: 12px; }

/* Two plans share the width; under ~420px of room they stack. */
.cr-picks.cr-picks--plans { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }
.cr-sheet__body .cr-picks.cr-picks--plans { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); margin-top: 16px; }

.cr-plan__code { margin-top: 12px; }
.cr-plan__status { margin-top: 24px; }
.cr-plan .cr-row--lines small { white-space: pre-line; }
.cr-plan__checkout { margin-top: 16px; }
.cr-plan__checkout .cr-btn { min-height: 48px; font-size: 0.9375rem; }

.cr-plan__codelink {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 2px;
    border: 0;
    background: none;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 700;
    color: var(--cr-accent);
    text-decoration: underline;
    text-decoration-color: var(--cr-accent-line);
    text-underline-offset: 4px;
    cursor: pointer;
}
.cr-plan__codelink:hover { text-decoration-color: currentColor; }

.cr-plan__applied {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: 52px;
    padding: 6px 6px 6px 16px;
    border: 1px dashed var(--cr-accent-line);
    border-radius: var(--cr-r);
    background: var(--cr-accent-soft);
}
.cr-plan__applied-t { margin: 0; min-width: 0; font-size: 0.875rem; line-height: 1.45; color: var(--cr-ink-2); overflow-wrap: anywhere; }
.cr-plan__applied-t b { color: var(--cr-ink); letter-spacing: 0.02em; }
.cr-plan__applied-t span { color: var(--cr-accent); font-weight: 700; margin-right: 2px; }
.cr-plan__remove {
    flex: none;
    min-height: 40px;
    padding: 0 12px;
    border: 0;
    border-radius: 999px;
    background: none;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--cr-ink-2);
    cursor: pointer;
}
.cr-plan__remove:hover { background: var(--cr-surface); color: var(--cr-ink); }
.cr-plan__claim { margin-top: 12px; min-height: 48px; }
.cr-plan__codelink:focus-visible,
.cr-plan__remove:focus-visible { outline: 2px solid var(--cr-gold); outline-offset: 2px; border-radius: 8px; }

/* The sheet: a little more air than the generic one, and a footer that is a
   thumb's width on a phone. */
.cr-sheet--plan .cr-sheet__head { padding: 16px 20px 8px; }
.cr-sheet--plan .cr-sheet__body { padding: 8px 20px 24px; }
.cr-sheet--plan .cr-sheet__foot { gap: 10px; padding: 16px 20px calc(16px + env(safe-area-inset-bottom, 0px)); }
.cr-sheet--plan .cr-sheet__foot > .cr-btn { min-height: 48px; padding: 12px 22px; }
.cr-sheet--plan .cr-label { font-size: 0.875rem; margin: 4px 0 8px; }
/* 16px stops iOS zooming the page when the field is focused. */
.cr-sheet--plan .cr-input { font-size: 1rem; min-height: 50px; padding: 12px 14px; letter-spacing: 0.04em; }
.cr-sheet--plan .cr-input::placeholder { letter-spacing: 0; color: var(--cr-ink-3); }
.cr-sheet--plan .cr-input[aria-invalid="true"] { border-color: var(--cr-danger); }
.cr-sheet--plan .cr-hint { margin-top: 8px; }
.cr-sheet--plan .cr-hint[role="alert"] { color: var(--cr-danger); font-weight: 600; }
.cr-sheet--plan .cr-plan__offer { padding: 16px 18px; }
.cr-sheet--plan .cr-plan__offer + .cr-hint { margin-top: 12px; }
.cr-plan__lead { margin: 4px 0 0; font-size: 0.9375rem; line-height: 1.6; color: var(--cr-ink); }
.cr-plan__lead b { font-weight: 700; }
.cr-plan__lead + .cr-hint { margin-top: 12px; }
.cr-plan__lead--gap { margin-top: 16px; }
.cr-plan__manager { margin-top: 16px; padding: 18px; }
.cr-plan__manager .cr-plan__lead { margin: 0; }
.cr-plan__remind { margin-top: 16px; min-height: 48px; }
.cr-plan__remind[disabled] { cursor: default; opacity: 0.7; }
.cr-plan__link {
    margin: 8px 0 0;
    padding: 12px 14px;
    border-radius: var(--cr-r);
    border: 1px solid var(--cr-line);
    background: var(--cr-bg);
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--cr-ink);
    overflow-wrap: anywhere;
    user-select: all;
    -webkit-user-select: all;
}
/* ── Children & safety: one card per child ─────────────────────────────
 * Head (picture, name, age and XP, Change), then the three rules that decide
 * what the child sees and who they can reach, each with its answer on the
 * right. The answer carries its state in words as well as colour.
 */
.cr-kids { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; }
.cr-kid {
    min-width: 0;
    background: var(--cr-surface);
    border: 1px solid var(--cr-line-soft);
    border-radius: var(--cr-r-lg);
    padding: 16px;
}
.cr-kid__head { display: flex; align-items: center; gap: 14px; }
.cr-kid__head .cr-face--lg { flex: none; }
.cr-kid__who { flex: 1 1 auto; min-width: 0; }
.cr-kid__who h3 { margin: 0; font-family: Fraunces, Georgia, serif; font-size: 1.125rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cr-kid__who p { margin: 2px 0 0; font-size: 0.875rem; color: var(--cr-ink-3); }
.cr-kid__head .cr-btn { flex: none; min-height: 40px; }
.cr-kid__rules { list-style: none; margin: 14px 0 0; padding: 0; border-top: 1px solid var(--cr-line-soft); }
.cr-kid__rules li {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    padding: 8px 0;
    border-bottom: 1px solid var(--cr-line-soft);
}
.cr-kid__rules li:last-child { border-bottom: 0; padding-bottom: 0; }
.cr-kid__ic { flex: none; width: 28px; text-align: center; font-size: 1rem; }
.cr-kid__label { flex: 1 1 auto; min-width: 0; font-size: 0.875rem; font-weight: 600; color: var(--cr-ink-2); }
.cr-kid__val {
    flex: none;
    max-width: 55%;
    padding: 3px 10px;
    border-radius: 999px;
    font-size: 0.875rem;
    font-weight: 700;
    text-align: right;
    overflow-wrap: anywhere;
}
.cr-kid__val--ok,
.cr-kid__val--on { background: var(--cr-ok-soft); color: var(--cr-ok); }
.cr-kid__val--set { background: var(--cr-accent-soft); color: var(--cr-accent); }
.cr-kid__val--off { background: var(--cr-surface-2); color: var(--cr-ink-2); }
.cr-kid__val--warn { background: var(--cr-danger-soft); color: var(--cr-danger); }

/* Family friends: the invite card sits under the heading, the list under it. */
.cr-friends__invite + .cr-friends__list,
.cr-friends__invite + .cr-empty { margin-top: 12px; }

/* Content age: two choices, then the brackets when "Choose a bracket" is on. */
.cr-content-age { margin: 18px 0 0; padding: 0; border: 0; min-width: 0; }
.cr-content-age legend { padding: 0; margin: 0 0 8px; }
.cr-opts { display: grid; gap: 8px; }
.cr-opt {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px;
    border: 1px solid var(--cr-line-soft);
    border-radius: var(--cr-r);
    background: var(--cr-surface);
    cursor: pointer;
}
.cr-opt:has(input:checked) { border-color: var(--cr-accent-line); background: var(--cr-accent-soft); }
.cr-opt input { flex: none; width: 20px; height: 20px; margin: 1px 0 0; accent-color: var(--cr-gold); }
.cr-opt__b { min-width: 0; }
.cr-opt__t { display: block; font-size: 0.9375rem; font-weight: 600; }
.cr-opt small { display: block; margin-top: 2px; font-size: 0.875rem; line-height: 1.45; color: var(--cr-ink-3); }
.cr-brackets { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
.cr-brackets[hidden] { display: none; }
.cr-bracket {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 48px;
    padding: 0 14px;
    border: 1px solid var(--cr-line-soft);
    border-radius: var(--cr-r);
    background: var(--cr-surface);
    font-size: 0.9375rem;
    font-weight: 600;
    cursor: pointer;
}
.cr-bracket:has(input:checked) { border-color: var(--cr-accent-line); background: var(--cr-accent-soft); }
.cr-bracket input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--cr-gold); }
.cr-opt:focus-within,
.cr-bracket:focus-within { outline: 2px solid var(--cr-gold); outline-offset: 2px; }

/* ── Gifts of Plus ─────────────────────────────────────────────────────
 * Screen: a warm invitation at the top, then each gift bought, then one card
 * per code — what it is, where it stands, and (while unused) Send / Options.
 * Sheets: groups separated by 24px, every label a short question.
 * Rhythm: 24 between groups, 12 inside a group, 4 under a title.
 */
.cr-gift-hero {
    margin-top: 24px;
    padding: 24px 20px 20px;
    border-radius: var(--cr-r-lg);
    border: 1px solid var(--cr-accent-line);
    background: linear-gradient(160deg, var(--cr-accent-soft), var(--cr-surface) 70%);
}
.cr-gift-hero__icon {
    display: grid; place-items: center;
    width: 48px; height: 48px; border-radius: 50%;
    background: var(--cr-surface); border: 1px solid var(--cr-accent-line);
    font-size: 1.375rem;
}
.cr-gift-hero h3 { margin: 16px 0 0; font-family: Fraunces, Georgia, serif; font-size: 1.375rem; font-weight: 600; line-height: 1.25; }
.cr-gift-hero p { margin: 6px 0 0; font-size: 0.9375rem; line-height: 1.55; color: var(--cr-ink-2); max-width: 46ch; }
.cr-gift-hero .cr-btn { margin-top: 20px; min-height: 48px; font-size: 0.9375rem; }
@media (min-width: 720px) { .cr-gift-hero .cr-btn { max-width: 320px; } }

.cr-gift-order { margin-top: 20px; }
.cr-gift-order + .cr-gift-order { margin-top: 28px; }
.cr-gift-order__head h4 { margin: 0; font-family: Fraunces, Georgia, serif; font-size: 1.0625rem; font-weight: 600; }
.cr-gift-order__head p { margin: 4px 0 0; font-size: 0.875rem; color: var(--cr-ink-3); }
.cr-gifts { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin-top: 12px; }

.cr-gift {
    min-width: 0;
    padding: 18px;
    border-radius: var(--cr-r-lg);
    border: 1px solid var(--cr-line-soft);
    background: var(--cr-surface);
}
.cr-gift--closed { background: var(--cr-surface-2); }
.cr-gift__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-width: 0; }
.cr-gift__badge {
    flex: none;
    padding: 4px 10px;
    border-radius: 999px;
    font-size: 0.875rem;
    font-weight: 700;
    line-height: 1.3;
}
.cr-gift__badge--set { background: var(--cr-accent-soft); color: var(--cr-accent); }
.cr-gift__badge--on { background: var(--cr-ok-soft); color: var(--cr-ok); }
.cr-gift__badge--off { background: var(--cr-surface); color: var(--cr-ink-2); border: 1px solid var(--cr-line-soft); }
.cr-gift__badge--warn { background: var(--cr-danger-soft); color: var(--cr-danger); }
.cr-gift__for { min-width: 0; font-size: 0.875rem; font-weight: 600; color: var(--cr-ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cr-gift__code {
    margin: 14px 0 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1.125rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    color: var(--cr-ink);
}
.cr-gift--closed .cr-gift__code { color: var(--cr-ink-3); }
.cr-gift__line { margin: 4px 0 0; font-size: 0.875rem; color: var(--cr-ink-3); }
.cr-gift__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; }
.cr-gift__actions .cr-btn { min-height: 44px; }
.cr-gift__empty { margin-top: 24px; }

/* Sheet groups: a short question, an optional hint, then the control. */
.cr-fgroup { margin: 24px 0 0; padding: 0; border: 0; min-width: 0; }
.cr-sheet__body > .cr-fgroup:first-child { margin-top: 4px; }
.cr-fgroup__t { display: block; margin: 0; padding: 0; font-size: 0.9375rem; font-weight: 700; color: var(--cr-ink); }
.cr-fgroup__h { margin: 4px 0 0; font-size: 0.875rem; color: var(--cr-ink-3); }
.cr-fgroup > .cr-lens,
.cr-fgroup > .cr-stack,
.cr-fgroup > .cr-gift__labelrow { margin-top: 12px; }
.cr-sheet--plan .cr-row small { font-size: 0.875rem; line-height: 1.5; }
.cr-sheet--plan .cr-row p { font-size: 0.9375rem; }

/* Gift length: four tiles, two by two. */
.cr-lens { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.cr-len {
    position: relative;
    display: grid;
    gap: 2px;
    padding: 16px;
    min-height: 76px;
    align-content: start;
    border-radius: var(--cr-r);
    border: 1px solid var(--cr-line-soft);
    background: var(--cr-surface);
    cursor: pointer;
}
.cr-len input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.cr-len:has(input:checked) { border: 2px solid var(--cr-gold); padding: 15px; background: var(--cr-accent-soft); }
.cr-len:has(input:focus-visible) { outline: 2px solid var(--cr-gold); outline-offset: 2px; }
.cr-len__t { font-size: 1rem; font-weight: 700; color: var(--cr-ink); }
.cr-len__p { font-size: 0.875rem; color: var(--cr-ink-2); }
.cr-len__tag {
    justify-self: start;
    margin-top: 6px;
    padding: 2px 8px; border-radius: 999px;
    font-size: 0.75rem; font-weight: 700;
    background: var(--cr-gold); color: #2a2012;
}

.cr-gift__qty { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.cr-gift__qty > div:first-child { min-width: 0; }
.cr-stepper { flex: none; display: inline-flex; align-items: center; border: 1px solid var(--cr-line); border-radius: 999px; background: var(--cr-surface); }
.cr-stepper__b {
    width: 44px; height: 44px;
    border: 0; background: none; border-radius: 999px;
    font: inherit; font-size: 1.25rem; font-weight: 700; color: var(--cr-ink);
    cursor: pointer;
}
.cr-stepper__b:disabled { color: var(--cr-ink-3); opacity: 0.45; cursor: not-allowed; }
.cr-stepper__b:focus-visible { outline: 2px solid var(--cr-gold); outline-offset: 2px; }
.cr-stepper__n { min-width: 32px; text-align: center; font-size: 1.0625rem; font-weight: 700; }

.cr-gift-total {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    margin-top: 24px;
    padding: 16px 18px;
    border-radius: var(--cr-r);
    background: var(--cr-surface-2);
}
.cr-gift-total > div { min-width: 0; }
.cr-gift-total__t { margin: 0; font-size: 0.9375rem; font-weight: 700; }
.cr-gift-total__d { margin: 2px 0 0; font-size: 0.875rem; color: var(--cr-ink-3); }
.cr-gift-total__a { flex: none; margin: 0; font-family: Fraunces, Georgia, serif; font-size: 1.5rem; font-weight: 600; }

/* The code in the Options sheet: the one thing to read out or copy. */
.cr-gift-codebox {
    margin-top: 4px;
    padding: 18px;
    border-radius: var(--cr-r-lg);
    border: 1px dashed var(--cr-accent-line);
    background: var(--cr-accent-soft);
    text-align: center;
}
.cr-gift-codebox__c {
    margin: 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1.375rem; font-weight: 700; letter-spacing: 0.1em;
    color: var(--cr-ink);
}
.cr-gift-codebox__l { margin: 6px 0 0; font-size: 0.875rem; color: var(--cr-ink-2); overflow-wrap: anywhere; user-select: all; -webkit-user-select: all; }
.cr-gift__labelrow { display: flex; gap: 8px; }
.cr-gift__labelrow .cr-input { flex: 1 1 auto; min-width: 0; }
.cr-gift__labelrow .cr-btn { flex: none; min-height: 50px; padding-left: 20px; padding-right: 20px; }

/* Month and year of birth, side by side. */
.cr-born { display: grid; grid-template-columns: 1.3fr 1fr; gap: 10px; }
.cr-born > * { min-width: 0; }

.cr-plan__tag { margin: 0; font-size: 0.875rem; font-weight: 700; letter-spacing: 0.06em; color: var(--cr-accent); }
.cr-plan__deal { margin: 4px 0 0; font-family: Fraunces, Georgia, serif; font-size: 1.25rem; font-weight: 600; line-height: 1.3; color: var(--cr-ink); }

@media (max-width: 719px) {
    .cr-sheet--plan .cr-sheet__foot { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.7fr); }
    .cr-sheet--plan .cr-sheet__foot > :only-child { grid-column: 1 / -1; }
    /* Keep / Cancel: two answers of equal weight in width; colour says which. */
    .cr-sheet--confirm .cr-sheet__foot { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .cr-sheet--plan .cr-sheet__foot > .cr-btn { width: 100%; padding-left: 12px; padding-right: 12px; }
}
@media (max-width: 379px) {
    .cr-sheet--plan .cr-sheet__foot { grid-template-columns: minmax(0, 1fr); }
    .cr-sheet--plan .cr-sheet__foot > .cr-btn--gold { order: -1; }
}
/* A sheet's first section heading has the sheet's own padding above it. */
.cr-sheet__body > .cr-sect:first-child { margin-top: 6px; }


/*
 * THE PICK TILE — the crest row, made answerable.
 *
 * Same surface, same radius, same 13/14 padding as .cr-row, with a mark in
 * the slot the row gives its face. A list of ANSWERS and a list of
 * DESTINATIONS must not look alike: one changes what you hold, the other
 * takes you somewhere.
 */
.cr-picks { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 9px; }
.cr-picks > * { min-width: 0; }
.cr-pick {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    width: 100%;
    text-align: left;
    padding: 13px 14px;
    min-height: 44px;
    border-radius: var(--cr-r);
    background: var(--cr-surface);
    border: 1px solid var(--cr-line-soft);
    font: inherit;
    color: inherit;
    cursor: pointer;
}
.cr-pick:hover:not([disabled]) { border-color: var(--cr-accent-line); }
.cr-pick:focus-visible { outline: 2px solid var(--cr-gold); outline-offset: 2px; }
.cr-pick__k {
    flex: none;
    width: 20px;
    height: 20px;
    margin-top: 1px;
    border-radius: 6px;
    border: 1.5px solid var(--cr-line);
    background: var(--cr-bg);
    display: grid;
    place-items: center;
    color: transparent;
}
/* One answer of several: a circle, because a square reads as "tick as many". */
.cr-pick__k--one { border-radius: 50%; }
.cr-pick__b { min-width: 0; }
.cr-pick__n { display: block; font-size: 0.9375rem; font-weight: 600; letter-spacing: -0.005em; }
.cr-pick__d { display: block; margin-top: 2px; font-size: 0.8125rem; color: var(--cr-ink-3); line-height: 1.45; }
/* Why you hold this, named on the tile — so "where did this come from?" is
   answered where it is asked, not in a footnote under the list. */
.cr-pick__w {
    display: inline-block;
    margin-top: 7px;
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 700;
    background: var(--cr-accent-soft);
    color: var(--cr-accent);
    border: 1px solid var(--cr-accent-line);
}
.cr-pick[aria-pressed="true"] { border-color: var(--cr-accent-line); background: var(--cr-accent-soft); }
.cr-pick[aria-pressed="true"] .cr-pick__k { background: var(--cr-gold); border-color: var(--cr-gold); color: #2a2012; }
/* Held, but not by your hand — granted by an answer above. Shown ON and not
   pressable: a control that springs back the moment you touch it reads as
   broken rather than as policy. */
.cr-pick.is-held { cursor: default; border-color: var(--cr-accent-line); background: var(--cr-accent-soft); }
.cr-pick.is-held .cr-pick__k { background: var(--cr-gold); border-color: var(--cr-gold); color: #2a2012; opacity: 0.55; }
/* Not available to this account at all. */
.cr-pick[disabled]:not(.is-held) { cursor: not-allowed; opacity: 0.55; background: var(--cr-surface-2); }

/* A question with no "none" answer says so beside the question, not in a
   sentence underneath that the eye skips on its way to the tiles. */
.cr-req {
    flex: none;
    padding: 4px 11px;
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 700;
    background: var(--cr-surface-2);
    color: var(--cr-ink-3);
}
.cr-req--due { background: var(--cr-accent-soft); color: var(--cr-accent); border: 1px solid var(--cr-accent-line); }

/*
 * THE SAVE BAR.
 *
 * Sticky, because the answers above it are longer than a screen and a Save
 * button you have to scroll back to is one people miss — which on this page
 * means leaving with the spaces they arrived with.
 */
.cr-savebar {
    position: sticky;
    bottom: 0;
    z-index: 15;
    margin-top: 26px;
    padding: 12px 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    background: var(--cr-bg);
    border-top: 1px solid var(--cr-line);
}
.cr-savebar p { margin: 0; font-size: 0.75rem; font-weight: 700; color: var(--cr-accent); }
.cr-savebar p:empty { display: none; }
.cr-savebar p.is-quiet { color: var(--cr-ink-3); font-weight: 600; }

/* First run only: one line, then out of the way for good. */
.cr-hail {
    display: flex;
    align-items: flex-start;
    gap: 11px;
    margin-top: 16px;
    padding: 13px 14px;
    border-radius: var(--cr-r);
    background: var(--cr-accent-soft);
    border: 1px solid var(--cr-accent-line);
}
.cr-hail p { margin: 0; font-size: 0.8125rem; line-height: 1.5; color: var(--cr-ink-2); }

.cr-stack > * { min-width: 0; }

.cr-face {
    width: 38px;
    height: 38px;
    flex: none;
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-size: 1rem;
    font-weight: 700;
    background: var(--cr-surface-2);
    color: var(--cr-ink-2);
    overflow: hidden;
}
.cr-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cr-face--lg { width: 56px; height: 56px; font-size: 1.375rem; }

.cr-sect { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 26px 0 12px; }
.cr-sect > * { min-width: 0; }
.cr-sect h3 { margin: 0; font-family: Fraunces, Georgia, serif; font-size: 1.1875rem; font-weight: 600; letter-spacing: -0.01em; }
.cr-sect p { margin: 3px 0 0; font-size: 0.8125rem; color: var(--cr-ink-3); line-height: 1.45; }
.cr-sect__a {
    flex: none;
    font-size: 0.8125rem;
    font-weight: 700;
    color: var(--cr-accent);
    text-decoration: none;
    background: none;
    border: 0;
    cursor: pointer;
    font-family: inherit;
    padding: 0;
}

.cr-empty { text-align: center; padding: 28px 16px; border: 1px dashed var(--cr-line); border-radius: var(--cr-r-lg); }
.cr-empty b { display: block; font-family: Fraunces, Georgia, serif; font-size: 1rem; font-weight: 600; }
.cr-empty p { margin: 5px auto 0; font-size: 0.8125rem; color: var(--cr-ink-3); max-width: 32ch; line-height: 1.55; }

.cr-panel { background: var(--cr-surface); border: 1px solid var(--cr-line-soft); border-radius: var(--cr-r-lg); padding: 16px; }
.cr-input {
    width: 100%;
    font: inherit;
    font-size: 0.9375rem;
    padding: 11px 13px;
    border-radius: var(--cr-r);
    border: 1px solid var(--cr-line);
    background: var(--cr-bg);
    color: var(--cr-ink);
}
.cr-label { display: block; font-size: 0.8125rem; font-weight: 700; margin-top: 14px; margin-bottom: 6px; }
.cr-hint { margin: 7px 0 0; font-size: 0.75rem; color: var(--cr-ink-3); line-height: 1.5; }

/* ── The jewel ──────────────────────────────────────────────────────── */
.cr-jewel {
    background: radial-gradient(125% 95% at 50% -12%, #3a2c66 0%, var(--cr-plum) 34%, var(--cr-deep) 100%);
    color: var(--cr-on-jewel);
    padding: 24px 22px 26px;
    text-align: center;
    position: relative;
    border-radius: 0 0 var(--cr-r-xl) var(--cr-r-xl);
}
.cr-sigil {
    width: 78px;
    height: 78px;
    margin: 0 auto;
    border-radius: 50%;
    display: grid;
    place-items: center;
    overflow: hidden;
    background: linear-gradient(158deg, var(--cr-gold-2), #b8842c);
    color: #2a2012;
    font-family: Fraunces, Georgia, serif;
    font-weight: 800;
    font-size: 2rem;
    box-shadow: 0 0 0 5px rgba(217, 164, 65, 0.13), 0 12px 26px -12px rgba(0, 0, 0, 0.85);
}
.cr-sigil img { width: 100%; height: 100%; object-fit: cover; }
.cr-jewel__name {
    margin: 15px 0 0;
    font-family: Fraunces, Georgia, serif;
    font-size: 1.625rem;
    font-weight: 800;
    letter-spacing: -0.018em;
    line-height: 1.15;
    color: var(--cr-on-jewel);
    overflow-wrap: anywhere;
}
.cr-jewel__est {
    margin: 5px 0 0;
    font-size: 0.75rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--cr-gold);
    font-weight: 700;
}
.cr-lvl { margin: 19px auto 0; max-width: 240px; }
.cr-lvl__bar { height: 6px; border-radius: 999px; background: rgba(244, 239, 228, 0.13); overflow: hidden; }
.cr-lvl__bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--cr-gold), var(--cr-gold-2)); }
.cr-lvl__row {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-top: 7px;
    font-size: 0.75rem;
    color: var(--cr-on-jewel-mute);
    font-variant-numeric: tabular-nums;
}
.cr-lvl__row > * { min-width: 0; }
.cr-lvl__row b { color: var(--cr-on-jewel); font-weight: 700; }

.cr-folk { display: flex; justify-content: center; flex-wrap: wrap; margin: 20px 0 0; }
.cr-folk > * { min-width: 0; }
.cr-folk .cr-face {
    width: 44px;
    height: 44px;
    border: 2px solid var(--cr-deep);
    margin-left: -9px;
    background: #3b2f63;
    color: var(--cr-on-jewel);
    cursor: pointer;
    padding: 0;
    font-size: 1.0625rem;
}
.cr-folk .cr-face:first-child { margin-left: 0; }

/* ── Needs you ──────────────────────────────────────────────────────── */
.cr-alerts { margin-top: -14px; position: relative; z-index: 2; display: flex; flex-direction: column; gap: 9px; }
.cr-alerts > * { min-width: 0; }
.cr-alert {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    background: linear-gradient(160deg, var(--cr-gold-2), var(--cr-gold));
    color: #241a06;
    border-radius: var(--cr-r);
    padding: 12px 14px;
    box-shadow: 0 14px 28px -16px rgba(0, 0, 0, 0.6);
    text-decoration: none;
}
.cr-alert > * { min-width: 0; }
.cr-alert__g {
    width: 34px;
    height: 34px;
    flex: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.4);
    display: grid;
    place-items: center;
    font-size: 1.0625rem;
}
.cr-alert__b { flex: 1 1 200px; min-width: 0; }
.cr-alert b { display: block; font-size: 0.875rem; font-weight: 800; }
.cr-alert small { display: block; font-size: 0.75rem; margin-top: 1px; opacity: 0.78; line-height: 1.45; }
.cr-alert__go { margin-left: auto; font-size: 1.0625rem; font-weight: 700; flex: none; }

/* ── Ledger ─────────────────────────────────────────────────────────── */
.cr-ledger { margin-top: 22px; border-top: 1px solid var(--cr-line); }
.cr-led {
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    padding: 15px 2px;
    border: 0;
    border-bottom: 1px solid var(--cr-line);
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}
.cr-led > * { min-width: 0; }
.cr-led:hover .cr-led__t { color: var(--cr-accent); }
.cr-led__g { width: 30px; flex: none; text-align: center; color: var(--cr-accent); font-size: 0.9375rem; }
.cr-led__t { display: block; font-family: Fraunces, Georgia, serif; font-size: 1.0625rem; font-weight: 600; letter-spacing: -0.01em; }
.cr-led small { display: block; font-size: 0.75rem; color: var(--cr-ink-3); margin-top: 2px; }
.cr-led__n { margin-left: auto; flex: none; font-family: Fraunces, Georgia, serif; font-size: 0.9375rem; color: var(--cr-accent); font-variant-numeric: tabular-nums; }

/* ── Trophy cabinet ─────────────────────────────────────────────────── */
/*
 * A rail on a phone, a cabinet on a desk.
 *
 * A horizontal scroller is right where the screen is narrower than the row:
 * you swipe, and the cut-off tile at the edge is the affordance telling you
 * so. On a 1000px page it is just a cramped strip with seven small tiles and
 * an eighth clipped for no reason, leaving most of the width empty — the
 * trophies stop looking like a cabinet and start looking like an overflow.
 *
 * So the rail is the small-screen case and the grid takes over with the room
 * to justify it. `auto-fill` rather than `auto-fit`: a household with three
 * trophies should see three trophy-sized tiles and the space where the rest
 * will go, not three tiles stretched across the page pretending to be a full
 * set. That empty space is the point of a cabinet.
 */
.cr-medals { display: flex; gap: 9px; overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }
.cr-medals::-webkit-scrollbar { display: none; }
.cr-medals > * { min-width: 0; }
.cr-medal { flex: 0 0 auto; width: 68px; text-align: center; text-decoration: none; color: inherit; }
.cr-medal__m {
    width: 100%;
    height: 76px;
    border-radius: 10px 10px 30px 30px;
    display: grid;
    place-items: center;
    font-size: 1.625rem;
    background: linear-gradient(180deg, var(--cr-surface-2), var(--cr-surface));
    border: 1px solid var(--cr-line);
}
.cr-medal--off .cr-medal__m { opacity: 0.42; filter: grayscale(1); }
/*
 * The caption gets a CLASS, not a bare element selector.
 *
 * Written as `.cr-medal span` it also matched `.cr-medal__m` — the art tile is
 * a span too — and (0,1,1) outranks the tile's own (0,1,0), so the trophy art
 * inherited the caption's 0.75rem. A 12px emoji adrift in a 92px box, which
 * read as "the trophies are not filling the space" because they were not.
 *
 * Two lines are reserved so a one-word name and a two-word name keep the same
 * baseline instead of the row jittering along its bottom edge.
 */
.cr-medal__n {
    display: block;
    margin-top: 7px;
    min-height: 2.1875rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--cr-ink-3);
    line-height: 1.35;
}

@media (min-width: 620px) {
    .cr-medals {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
        gap: 14px 12px;
        overflow: visible;
        padding-bottom: 0;
    }
    .cr-medal { width: auto; }
    .cr-medal__m { height: 92px; font-size: 1.875rem; }
}

/* ── Screens ────────────────────────────────────────────────────────── */
.cr-screen { display: none; }
.cr-screen.is-on { display: block; }
.cr-crumb {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 13px 0;
    margin-bottom: 4px;
    background: var(--cr-bg);
    border-bottom: 1px solid var(--cr-line);
}
.cr-crumb > * { min-width: 0; }
.cr-crumb h2 { margin: 0; font-family: Fraunces, Georgia, serif; font-size: 1.125rem; font-weight: 600; letter-spacing: -0.01em; }
.cr-back {
    width: 34px;
    height: 34px;
    flex: none;
    border-radius: 50%;
    border: 1px solid var(--cr-line);
    background: var(--cr-surface);
    color: var(--cr-ink);
    font-size: 1rem;
    cursor: pointer;
    line-height: 1;
}

.cr-quest { display: block; padding: 14px; border-radius: var(--cr-r-lg); background: var(--cr-surface); border: 1px solid var(--cr-line-soft); text-decoration: none; color: inherit; }
.cr-quest b { font-family: Fraunces, Georgia, serif; font-size: 1rem; font-weight: 600; }
/* display:block, because it is a <span>: without it the height is ignored,
   the bar collapses to nothing, and every quest card showed its percentage
   in words over an empty gap where the measure should be. */
.cr-quest__m { display: block; height: 6px; border-radius: 999px; background: var(--cr-surface-2); overflow: hidden; margin-top: 10px; }
.cr-quest__m i { display: block; height: 100%; background: linear-gradient(90deg, var(--cr-gold), var(--cr-gold-2)); }
.cr-quest small { display: block; margin-top: 7px; font-size: 0.75rem; color: var(--cr-ink-3); font-variant-numeric: tabular-nums; }

.cr-note { background: var(--cr-deep); color: var(--cr-on-jewel); border-radius: var(--cr-r-lg); padding: 16px 18px; margin-top: 22px; }
.cr-note h4 { margin: 0; font-family: Fraunces, Georgia, serif; font-size: 1rem; font-weight: 600; color: var(--cr-on-jewel); }
.cr-note p { margin: 7px 0 0; font-size: 0.8125rem; line-height: 1.65; color: var(--cr-on-jewel-mute); }

.cr-copy {
    margin-top: 9px;
    padding: 12px 14px;
    border-radius: var(--cr-r);
    background: var(--cr-accent-soft);
    border: 1px solid var(--cr-accent-line);
}
.cr-copy p { margin: 0; font-size: 0.8125rem; font-weight: 700; }
.cr-copy code { display: block; margin-top: 6px; font-size: 0.75rem; overflow-wrap: anywhere; color: var(--cr-ink-2); }

/* ── Sheet ──────────────────────────────────────────────────────────── */
.cr-scrim {
    position: fixed;
    inset: 0;
    z-index: 300;
    background: rgba(18, 14, 36, 0.62);
    display: flex;
    align-items: flex-end;
    justify-content: center;
}
.cr-scrim[hidden] { display: none; }
.cr-sheet {
    width: 100%;
    max-width: 520px;
    max-height: 88dvh;
    background: var(--cr-surface);
    color: var(--cr-ink);
    border-radius: var(--cr-r-xl) var(--cr-r-xl) 0 0;
    display: flex;
    flex-direction: column;
    box-shadow: var(--cr-shadow-lg);
}
.cr-sheet > * { min-width: 0; }
.cr-sheet__grip { width: 38px; height: 4px; border-radius: 999px; background: var(--cr-line); margin: 10px auto 0; flex: none; }
.cr-sheet__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 18px 8px; }
.cr-sheet__head h3 { margin: 0; font-family: Fraunces, Georgia, serif; font-size: 1.1875rem; font-weight: 600; }
.cr-sheet__body { padding: 6px 18px 16px; overflow-y: auto; }
.cr-sheet__foot { display: flex; gap: 9px; justify-content: flex-end; flex-wrap: wrap; padding: 13px 18px; border-top: 1px solid var(--cr-line); }
.cr-sheet__foot > * { min-width: 0; }
.cr-x { width: 34px; height: 34px; flex: none; border-radius: 50%; border: 1px solid var(--cr-line); background: var(--cr-bg); color: var(--cr-ink); cursor: pointer; font-size: 0.875rem; }

@media (min-width: 720px) {
    .cr-scrim { align-items: center; padding: 26px; }
    .cr-sheet { border-radius: var(--cr-r-xl); }
    .cr-sheet__grip { display: none; }
}

/* ── Desktop: the jewel stands up as a rail ───────────────────────────
 *
 * The two ledgers swap, and the DEFAULT must be declared before the media
 * query that overrides it. Written the other way round — the rule below the
 * query — it has equal specificity and wins by source order, so the rail
 * ledger stays hidden on desktop and nothing says why.
 */
.cr-rail-ledger { display: none; }

/* ── The jewel is anchored, not floating (phone) ──────────────────────
 *
 * Its top corners are square and its bottom corners are round, which is a
 * shape that only reads correctly when the flat edge is against something.
 * Sitting 12px below the top of the page with 16px of paper either side, it
 * looked like a card with two corners forgotten. It runs to the edges and
 * to the top instead, so the round bottom is what separates it from the page.
 *
 * The page's own side padding is cancelled rather than removed, because every
 * other zone on the page still wants it. On desktop the jewel becomes a rail
 * and takes all four corners back, so this is scoped below that breakpoint.
 */
@media (max-width: 899.98px) {
    body[data-module="profile"] .ui-page { padding-top: 0; }
    body[data-module="profile"] .cr-jewel {
        /* Exactly the page's own side padding, which steps up at 640px —
           a single hard-coded -16px left an 8px shoulder on a tablet. */
        margin-inline: calc(var(--ui-s-4) * -1);
        border-radius: 0 0 var(--cr-r-xl) var(--cr-r-xl);
    }
}
@media (min-width: 640px) and (max-width: 899.98px) {
    body[data-module="profile"] .cr-jewel { margin-inline: calc(var(--ui-s-6) * -1); }
}

@media (min-width: 900px) {
    .crest__shell {
        display: grid;
        grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
        gap: 28px;
        align-items: start;
        padding-block: 20px;
    }
    .crest__rail { position: sticky; top: 16px; }
    .cr-jewel { border-radius: var(--cr-r-xl); }
    .cr-rail-ledger { display: block; }
    .cr-home-ledger { display: none; }
    .cr-crumb { position: static; background: none; border: 0; padding: 0 0 6px; }
    .cr-crumb .cr-back { display: none; }
    .cr-crumb h2 { font-size: 1.625rem; }
}

@media (prefers-reduced-motion: reduce) {
    .cr-row, .cr-btn, .cr-led { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE YOU DRAWER WEARS THE CREST.

   Everything below restates a .ui-sheet rule in the crest's tokens. It sits
   at the end of the file deliberately: the base drawer rules are ~5000 lines
   up and carry the same specificity, so the cascade decides this, and the
   cascade is settled by order.

   Only the drawer. The sheets the rest of the app opens (.ui-sheet-modal)
   keep the app's palette — they belong to the page that raised them.
   ═══════════════════════════════════════════════════════════════════════ */
.ui-sheet__panel {
    background: var(--cr-bg);
    color: var(--cr-ink);
    border-top: 1px solid var(--cr-line);
    border-top-left-radius: var(--cr-r-xl);
    border-top-right-radius: var(--cr-r-xl);
}
.ui-sheet__grip { background: var(--cr-line); }
.ui-sheet__panel .ui-nav__menu-label { color: var(--cr-ink-3); }
.ui-sheet__panel .ui-nav__menu-sep { background: var(--cr-line); }

/* The name, set the way the crest sets a name: a serif, and not uppercase —
   a person's name in tracked-out capitals reads as a filing label. */
.ui-sheet__who {
    font-family: Fraunces, "Plus Jakarta Sans", Georgia, serif;
    font-weight: 800;
    color: var(--cr-ink);
}

.ui-sheet__panel .ui-nav__menu-link { color: var(--cr-ink); }
.ui-sheet__panel .ui-nav__menu-link:hover { background: var(--cr-accent-soft); }
.ui-sheet__panel .ui-nav__menu-link--danger { color: var(--cr-danger); }
.ui-sheet__panel .ui-nav__menu-link--danger:hover { background: var(--cr-danger-soft); }
.ui-sheet__panel .ui-nav__menu-count {
    background: var(--cr-accent-soft);
    color: var(--cr-accent);
}

/*
 * Light / Dark: the crest's pill pair, gold when chosen.
 *
 * A rounded rectangle tinted with the app's accent is what every other
 * multiple choice in the product looks like; the crest answers a two-way
 * question with a gold pill, and this is a two-way question.
 */
.ui-sheet__choice-btn {
    border-radius: 999px;
    border-color: var(--cr-line);
    background: var(--cr-surface);
    color: var(--cr-ink);
}
.ui-sheet__choice-btn:hover { background: var(--cr-surface-2); }
.ui-sheet__choice-btn[aria-pressed="true"] {
    background: linear-gradient(160deg, var(--cr-gold-2), var(--cr-gold));
    border-color: transparent;
    color: #2a2012;
    font-weight: 700;
}

/*
 * Text size keeps its four A's — each label drawn at the size it sets, so you
 * choose by looking rather than by decoding "Larger". That is better than the
 * plain pills the prototype had, so only the colours change here.
 */
.ui-sheet__textsize-btn {
    border-radius: var(--cr-r);
    border-color: var(--cr-line);
    background: var(--cr-surface);
    color: var(--cr-ink);
}
.ui-sheet__textsize-btn:hover { background: var(--cr-surface-2); }
.ui-sheet__textsize-btn[aria-pressed="true"] {
    border-color: var(--cr-accent-line);
    background: var(--cr-accent-soft);
}
.ui-sheet__textsize-name { color: var(--cr-ink-3); }
.ui-sheet__textsize-btn[aria-pressed="true"] .ui-sheet__textsize-name { color: var(--cr-accent); }

.ui-sheet__close {
    border-radius: 999px;
    border-color: var(--cr-line);
    background: var(--cr-surface);
    color: var(--cr-ink);
}
.ui-sheet__close:hover { background: var(--cr-surface-2); }

/*
 * THE ACCOUNT ROWS ARE THE CREST'S ROW, NOT A MENU LINE.
 *
 * They were bare text on the panel's own background — the desktop dropdown's
 * treatment, which is right for a dropdown and wrong here. The profile this
 * drawer opens into lists everything as a filled card with a face and a
 * chevron, and a row of plain words beside it reads as a different product.
 *
 * Same geometry as .cr-row: 13/14 padding, 14px radius, 13px gap, a 38px
 * round face.
 */
.ui-sheet__account { gap: 9px; }

.ui-sheet__row {
    display: flex;
    align-items: center;
    gap: 13px;
    width: 100%;
    padding: 13px 14px;
    border-radius: var(--cr-r);
    background: var(--cr-surface);
    border: 1px solid var(--cr-line-soft);
    color: var(--cr-ink);
    font: inherit;
    font-size: 0.9375rem;
    font-weight: 600;
    letter-spacing: -0.005em;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}
.ui-sheet__row:hover { border-color: var(--cr-accent-line); background: var(--cr-accent-soft); }
.ui-sheet__row:focus-visible { outline: 2px solid var(--cr-gold); outline-offset: 2px; }

.ui-sheet__row-face {
    flex: none;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-size: 1rem;
    background: var(--cr-surface-2);
    color: var(--cr-ink-2);
}
.ui-sheet__row-b { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px; }
.ui-sheet__row-car { flex: none; color: var(--cr-ink-3); font-size: 0.9375rem; }

/* Log out keeps the card, and only the word is red — a full red card for the
   least dangerous destructive action in the product is a shout. */
.ui-sheet__row--danger { color: var(--cr-danger); justify-content: center; }
.ui-sheet__row--danger:hover { background: var(--cr-danger-soft); border-color: transparent; }

/* Log out and Close share the last line, so they sit side by side rather than
   the row stretching the full width above a button. */
.ui-sheet__account-row { display: flex; align-items: center; gap: 9px; }
.ui-sheet__account-row > .ui-sheet__row--danger { flex: 1 1 auto; width: auto; }

/* ═══════════════════════════════════════════════════════════════════════
   A CREST-SKINNED SHEET, FOR ANYWHERE.

   .ui-sheet is the You drawer and is hidden above 900px on purpose — the
   header carries its contents there. Switch user is reachable from that
   header too, so it cannot live on that chassis. It uses .ui-sheet-modal,
   which the app already raises from the bottom on a phone and centres on a
   desktop, and this modifier gives that chassis the crest's skin without
   touching any other modal built on it.
   ═══════════════════════════════════════════════════════════════════════ */
.ui-sheet-modal--crest .ui-sheet-modal__panel {
    background: var(--cr-bg);
    color: var(--cr-ink);
    border-color: var(--cr-line);
    padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
}
.ui-sheet-modal--crest .ui-sheet-modal__grip { background: var(--cr-line); }
.ui-sheet-modal--crest .ui-nav__menu-label { color: var(--cr-ink-3); }
.ui-sheet-modal--crest .ui-nav__menu-sep { background: var(--cr-line); }

/*
 * The head's top padding is NOT the whole gap above the title.
 *
 * On a phone the grip sits above it and contributes 16px of its own
 * (10px margin + 4px bar + 2px), so 6px here read as 22px and looked right.
 * The desktop rule at the top of this file hides the grip -- there is no
 * screen edge to gesture back to -- and the panel itself has no top padding,
 * so all 16px of that vanished and the title was left 6px from the panel's
 * edge, tighter than any other sheet in the app.
 *
 * So: 10px here, and the desktop block below puts back what the grip was
 * standing in for. Both ends land at ~26px above the title.
 */
.ui-crestsheet__head { padding: 10px 18px 0; }

@media (min-width: 640px) {
    .ui-crestsheet__head { padding-top: var(--ui-s-6); }
}
.ui-crestsheet__head .ui-sheet__who { padding: 0; margin: 0; font-size: 1.1875rem; }
.ui-crestsheet__sub {
    margin: 4px 0 0;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--cr-ink-3);
}
.ui-crestsheet__body { padding: 0 18px; overflow-y: auto; }
.ui-crestsheet__body .ui-nav__menu-label { padding-left: 0; margin-top: var(--ui-s-4); }
.ui-crestsheet__foot {
    display: flex;
    justify-content: flex-end;
    gap: 9px;
    padding: 13px 18px 0;
    margin-top: 4px;
    border-top: 1px solid var(--cr-line);
}

/*
 * A PERSON'S ROW: the face is theirs, and the two lines stack.
 *
 * .ui-sheet__row-b is a flex ROW for the account menu, where the only second
 * thing is an unread count sitting beside the label. A person needs their
 * name over what they are, so this variant stacks it — the same shape the
 * crest's own member lists use.
 */
.ui-sheet__row--person .ui-sheet__row-b { display: block; }
.ui-sheet__row-t { display: block; font-size: 0.9375rem; font-weight: 600; }
.ui-sheet__row--person small {
    display: block;
    margin-top: 2px;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--cr-ink-3);
    line-height: 1.45;
}
.ui-sheet__row-face { overflow: hidden; }
.ui-sheet__row-face img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The profile you are already using is not a destination, so it does not get
   a chevron. It says so instead, and does not look pressable. */
.ui-sheet__row--now { cursor: default; border-color: var(--cr-accent-line); background: var(--cr-accent-soft); }
.ui-sheet__row--now:hover { border-color: var(--cr-accent-line); background: var(--cr-accent-soft); }
.ui-sheet__row-now {
    flex: none;
    padding: 4px 11px;
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 700;
    background: var(--cr-surface);
    color: var(--cr-accent);
    border: 1px solid var(--cr-accent-line);
}

/* ═══════════════════════════════════════════════════════════════════════
   THE CREST, BEFORE IT HAS ANY DATA.

   The page opened on a centred card reading "Opening your household…",
   which told somebody what the computer was busy with rather than what was
   about to appear, and then threw itself away for a layout that shared none
   of its shape — so every load ended in a jump.

   This follows the rule the post and group skeletons already set: the
   CONTAINER IS REAL. The jewel is the real jewel, plum and rounded, the
   ledger is the real ledger with its real rules between rows. Only the
   words and the faces are stand-ins, so the page assembles rather than
   flashes, and nothing moves when the answer arrives.
   ═══════════════════════════════════════════════════════════════════════ */

/*
 * On the jewel, the page's skeleton greys are near-white.
 *
 * .ui-skeleton is built from --ui-surface-2/3, which are the pale end of the
 * light theme. Laid on deep plum they read as solid blocks rather than as
 * absences. Same animation and the same reduced-motion escape; only the two
 * ends of the gradient change, to the hairline the jewel already uses.
 */
.cr-jewel .ui-skeleton {
    background: linear-gradient(90deg,
        rgba(244, 239, 228, 0.09) 25%,
        rgba(244, 239, 228, 0.20) 37%,
        rgba(244, 239, 228, 0.09) 63%);
    background-size: 400% 100%;
}
@media (prefers-reduced-motion: reduce) {
    .cr-jewel .ui-skeleton { background: rgba(244, 239, 228, 0.12); }
}

/* Centred stand-ins for the name and the line under it, at the widths the
   real ones usually take — a full-width bar would promise a longer name than
   any household has. */
.cr-skel-name { width: 62%; max-width: 15rem; height: 26px; margin: 15px auto 0; border-radius: var(--ui-r-sm); }
.cr-skel-est  { width: 42%; max-width: 11rem; height: 10px; margin: 9px auto 0; }

/* The sigil's own circle, in place of the gold one. */
.cr-skel-sigil { width: 78px; height: 78px; margin: 0 auto; }

/* The level row: the bar is the real bar's height, the two figures beside it
   sit where Level and XP do. */
.cr-skel-lvlrow { display: flex; justify-content: space-between; margin-top: 7px; }
.cr-skel-lvlrow > * { width: 5.5rem; height: 10px; }

/* Faces overlap exactly as the real strip does, so the row neither grows nor
   shifts when the household arrives. */
.cr-skel-folk { display: flex; justify-content: center; margin: 20px 0 0; }
.cr-skel-folk > * {
    width: 44px;
    height: 44px;
    border: 2px solid var(--cr-deep);
    border-radius: 999px;
    margin-left: -9px;
}
.cr-skel-folk > :first-child { margin-left: 0; }

/* A ledger row keeps the real one's height and its rule, so the five rules
   below the jewel do not move by a pixel. */
.cr-skel-led {
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    padding: 15px 2px;
    border-bottom: 1px solid var(--cr-line);
}
.cr-skel-led__g { width: 30px; height: 15px; flex: none; }
.cr-skel-led__b { flex: 1 1 auto; min-width: 0; display: grid; gap: 7px; }
.cr-skel-led__t { width: 42%; max-width: 11rem; height: 14px; }
.cr-skel-led__s { width: 62%; max-width: 15rem; height: 10px; }
.cr-skel-led__n { width: 12px; height: 12px; flex: none; }

/* ════════════════════════════════════════════════════════════════════════
   THE CABINET — /badges and /badges/:slug
   ════════════════════════════════════════════════════════════════════════

   The crest's world, one route along. It uses the --cr-* palette declared
   above rather than a second one of its own: the trophy cabinet on the
   profile and the trophy cabinet here are the same cabinet, and two
   palettes for one object is how they drift apart.

   Two ideas carry the page.

   1. A TROPHY IS AN OBJECT. It keeps its metal in both themes, because an
      emblem that repaints itself isn't one. What changes is the room it
      stands in — warm paper by day, deep plum at night.

   2. WHAT YOU HAVE NOT WON IS A SPACE, NOT A GHOST. The page before this
      drew an unearned badge as the earned card at lower opacity, which
      reads as a broken medal rather than an empty place on a shelf. Here
      it is a socket cut into the shelf with the glyph ghosted into it.

   Nothing on the wall is a card. The only line is the shelf rule under
   each row, so the full sockets count themselves before a word is read.
   ════════════════════════════════════════════════════════════════════════ */

.cabinet { background: var(--cr-bg); color: var(--cr-ink); min-height: 100vh; }
.cabinet__shell { width: 100%; max-width: 1080px; margin-inline: auto; }
.cabinet__shell > * { min-width: 0; }

/* ── The way back ────────────────────────────────────────────────────────
   Nobody opens this page cold: they arrive from the crest, from a quest
   they have just finished, or from the notification that said they had
   earned something. So the first control on a phone is the way out, and
   it is the same arrow every crest screen uses. On desktop it goes — the
   browser's own Back sits an inch away — and the title takes its size. */
.cab-top {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 13px 0;
    margin-bottom: 4px;
    background: var(--cr-bg);
    border-bottom: 1px solid var(--cr-line);
}
.cab-top > * { min-width: 0; }
.cab-top h1 {
    margin: 0;
    font-family: Fraunces, Georgia, serif;
    font-size: 1.125rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.cab-back {
    width: 36px;
    height: 36px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: 50%;
    border: 1px solid var(--cr-line);
    background: var(--cr-surface);
    color: var(--cr-ink);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    text-decoration: none;
}
.cab-back:hover { border-color: var(--cr-accent-line); background: var(--cr-accent-soft); color: var(--cr-accent); }
.cab-back:focus-visible,
.cab-tro:focus-visible,
.cab-next:focus-visible,
.cab-switch button:focus-visible,
.cab-sift button:focus-visible,
.cab-folk:focus-visible { outline: 2px solid var(--cr-gold); outline-offset: 2px; }

/* ── The case ────────────────────────────────────────────────────────────
   The dark head, and the only place a total lives. It answers three
   things in the order somebody wants them: what am I, how full is the
   cabinet, and what is next. The page it replaced opened on "Your
   Achievement Level / Loading achievements… / 0 / 7". */
.cab-case {
    position: relative;
    overflow: hidden;
    border-radius: var(--cr-r-xl);
    padding: 20px;
    color: var(--cr-on-jewel);
    background: radial-gradient(125% 100% at 50% -14%, #3a2c66 0%, var(--cr-plum) 36%, var(--cr-deep) 100%);
}
.cab-case::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: radial-gradient(60% 50% at 84% 8%, rgba(240, 208, 138, 0.16), transparent 70%);
}
.cab-case > * { position: relative; z-index: 1; min-width: 0; }
.cab-case__eyebrow {
    margin: 0;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--cr-gold);
}
.cab-case__rank {
    margin: 7px 0 0;
    font-family: Fraunces, Georgia, serif;
    font-size: 1.625rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.1;
}
.cab-case__who { margin: 6px 0 0; font-size: 0.8125rem; color: var(--cr-on-jewel-mute); }

/* The count, as a row of sockets: the cabinet in miniature. "5 / 12" is a
   fraction; this is a shelf you can see the gaps in. */
.cab-pips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 16px; }
.cab-pips > * { min-width: 0; }
.cab-pips i { width: 13px; height: 13px; border-radius: 50%; background: rgba(244, 239, 228, 0.1); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5); }
.cab-pips i.is-on { background: linear-gradient(158deg, var(--cr-gold-2), #a9761f); box-shadow: 0 0 0 1px rgba(240, 208, 138, 0.3), 0 2px 5px rgba(0, 0, 0, 0.45); }
.cab-case__tally { margin: 11px 0 0; font-size: 0.8125rem; color: var(--cr-on-jewel-mute); font-variant-numeric: tabular-nums; }
.cab-case__tally b { color: var(--cr-on-jewel); font-family: Fraunces, Georgia, serif; font-size: 0.9375rem; font-weight: 600; }

/* ONE next thing, and it is the closest. A page of everything somebody has
   not got is not encouragement. */
.cab-next {
    display: flex;
    align-items: center;
    gap: 13px;
    width: 100%;
    margin-top: 18px;
    padding: 16px 0 0;
    border: 0;
    border-top: 1px solid var(--cr-jewel-line);
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
    text-decoration: none;
}
.cab-next > * { min-width: 0; }
.cab-next__b { flex: 1 1 auto; min-width: 0; }
.cab-next__k { margin: 0; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--cr-on-jewel-mute); }
.cab-next__t { display: block; margin-top: 3px; font-family: Fraunces, Georgia, serif; font-size: 1.0625rem; font-weight: 600; letter-spacing: -0.01em; }
.cab-next:hover .cab-next__t { color: var(--cr-gold-2); }
.cab-next__m { display: block; height: 5px; border-radius: 999px; background: rgba(244, 239, 228, 0.12); overflow: hidden; margin-top: 9px; }
.cab-next__m i { display: block; height: 100%; background: linear-gradient(90deg, #a9761f, var(--cr-gold-2)); }
.cab-next small { display: block; margin-top: 6px; font-size: 0.75rem; color: var(--cr-on-jewel-mute); font-variant-numeric: tabular-nums; }

/* ── The object ──────────────────────────────────────────────────────────
   Struck, or an empty socket. Nothing between the two. */
.cab-med { position: relative; display: block; width: 68px; flex: none; }
/* display:block, because it is a <span>: an inline box ignores the height
   and every medal on the wall collapses to a line of text. */
.cab-med__r {
    display: block;
    position: relative;
    width: 68px;
    height: 68px;
    border-radius: 50%;
    padding: 4px;
    background: conic-gradient(from -90deg, var(--cr-gold) calc(var(--cab-p, 0) * 1%), rgba(128, 116, 160, 0.22) 0);
}
.cab-med__d {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-size: 1.625rem;
    line-height: 1;
    overflow: hidden;
}
.cab-med__g { filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.28)); }
.cab-med__d img { width: 100%; height: 100%; object-fit: cover; }

/* Struck: a rim, a face, and one glint across the top left. The tier is in
   the metal — it is the only place a tier has ever meant anything. */
.cab-med--struck .cab-med__r { background: none; padding: 0; }
.cab-med--struck .cab-med__d {
    background:
        radial-gradient(70% 60% at 30% 22%, rgba(255, 255, 255, 0.55), transparent 60%),
        conic-gradient(from 210deg, var(--cab-m3), var(--cab-m2) 22%, var(--cab-m1) 48%, var(--cab-m3) 62%, var(--cab-m2) 84%, var(--cab-m3));
    box-shadow:
        inset 0 0 0 2.5px rgba(255, 255, 255, 0.28),
        inset 0 0 0 5px var(--cab-m3),
        0 3px 10px -2px rgba(0, 0, 0, 0.45),
        0 10px 22px -12px rgba(0, 0, 0, 0.6);
}
.cab-med--gold { --cab-m1: #d9a441; --cab-m2: #f0d08a; --cab-m3: #a9761f; }
.cab-med--silver { --cab-m1: #c3c0d2; --cab-m2: #eceaf3; --cab-m3: #8a86a0; }
.cab-med--bronze { --cab-m1: #bf8552; --cab-m2: #e3b489; --cab-m3: #8a5c33; }

/* A socket: a recess in the shelf, with the glyph ghosted into it. */
.cab-med--empty .cab-med__d {
    background: var(--cr-surface-2);
    box-shadow: inset 0 3px 7px rgba(36, 28, 51, 0.17), inset 0 0 0 1px var(--cr-line);
}
.cab-med--empty .cab-med__g { opacity: 0.3; filter: grayscale(1); }
/* On the dark head the same socket must be a hole in THAT surface, not a
   pale disc sitting on it — and it must be opaque, or the progress ring
   shows through its own gap and reads as a filled pie. */
.cab-case .cab-med--empty .cab-med__d,
.cab-plinth .cab-med--empty .cab-med__d {
    background: #1b1436;
    box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.75), inset 0 0 0 1px var(--cr-jewel-line);
}
html[data-ui-theme="dark"] .cab-med--empty .cab-med__d {
    box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.55), inset 0 0 0 1px var(--cr-line);
}

.cab-med--sm { width: 36px; }
.cab-med--sm .cab-med__r { width: 36px; height: 36px; padding: 2px; }
.cab-med--sm .cab-med__d { font-size: 0.9375rem; }
.cab-med--lg { width: 108px; }
.cab-med--lg .cab-med__r { width: 108px; height: 108px; padding: 6px; }
.cab-med--lg .cab-med__d { font-size: 2.625rem; }

/* The percentage, printed on the ring it belongs to. */
.cab-med__p {
    position: absolute;
    left: 50%;
    bottom: -7px;
    transform: translateX(-50%);
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    background: var(--cr-surface);
    border: 1px solid var(--cr-accent-line);
    color: var(--cr-accent);
}
.cab-plinth .cab-med__p { background: rgba(18, 14, 36, 0.85); border-color: rgba(240, 208, 138, 0.4); color: var(--cr-gold-2); }

/* ── The shelves ─────────────────────────────────────────────────────────
   No cards. A shelf rule runs under every row and the objects stand on
   it, so the eye counts full sockets against empty ones without reading a
   single label. Two columns at 320px, four once there is room. */
.cab-shelf { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; margin-top: 2px; }
.cab-shelf > * { min-width: 0; }
.cab-tro {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px 8px;
    border: 0;
    border-bottom: 1px solid var(--cr-line);
    background: none;
    color: inherit;
    font: inherit;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: transform var(--ui-fast, 140ms) var(--ui-ease, ease);
}
.cab-tro > * { min-width: 0; }
.cab-tro:hover { transform: translateY(-3px); }
.cab-tro:hover .cab-tro__n { color: var(--cr-accent); }
.cab-tro__n {
    display: block;
    margin-top: 16px;
    font-family: Fraunces, Georgia, serif;
    font-size: 0.9375rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    line-height: 1.25;
}
.cab-tro__d { display: block; margin-top: 5px; font-size: 0.75rem; line-height: 1.45; color: var(--cr-ink-3); max-width: 22ch; }
.cab-tro__s { display: block; margin-top: 9px; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: var(--cr-ink-3); }
.cab-tro--on .cab-tro__s,
.cab-tro--near .cab-tro__s { color: var(--cr-accent); }
/* Twice-won is a fact about the object, the way a second bar rides on a
   ribbon — not a status chip in the corner of a card. */
.cab-tro__x {
    position: absolute;
    top: 16px;
    left: calc(50% + 20px);
    z-index: 2;
    min-width: 24px;
    height: 24px;
    padding: 0 6px;
    display: grid;
    place-items: center;
    border-radius: 999px;
    border: 1px solid var(--cr-jewel-line);
    background: var(--cr-deep);
    color: var(--cr-gold-2);
    font-size: 0.75rem;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

/* ── Section headings, shared by every shelf ─────────────────────────── */
.cab-sect { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 24px 0 10px; }
.cab-sect > * { min-width: 0; }
.cab-sect h2 { margin: 0; font-family: Fraunces, Georgia, serif; font-size: 1.25rem; font-weight: 600; letter-spacing: -0.01em; }
.cab-sect p { margin: 4px 0 0; font-size: 0.8125rem; line-height: 1.45; color: var(--cr-ink-3); max-width: 46ch; }
.cab-sect__n { flex: none; font-family: Fraunces, Georgia, serif; font-size: 0.9375rem; font-weight: 600; color: var(--cr-accent); font-variant-numeric: tabular-nums; }

/* ── The shelf switch ────────────────────────────────────────────────────
   Three shelves, named for what stands on them. The tabs this replaced
   read "My Badges · Legacy 5/7 · Family": one named after where the code
   came from, one carrying its own score inside its label. */
.cab-switch {
    display: flex;
    gap: 4px;
    padding: 4px;
    margin-top: 18px;
    border-radius: 999px;
    background: var(--cr-surface-2);
}
.cab-switch > * { min-width: 0; }
.cab-switch button {
    flex: 1 1 0;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 9px 6px;
    min-height: 40px;
    border: 0;
    border-radius: 999px;
    background: none;
    color: var(--cr-ink-3);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 700;
    white-space: nowrap;
    cursor: pointer;
}
.cab-switch button b { font-size: 0.75rem; font-weight: 800; opacity: 0.75; font-variant-numeric: tabular-nums; }
.cab-switch button[aria-pressed="true"] { background: var(--cr-surface); color: var(--cr-ink); box-shadow: var(--cr-shadow); }
.cab-switch button[aria-pressed="true"] b { color: var(--cr-accent); opacity: 1; }

/* ── The sift ────────────────────────────────────────────────────────────
   A filter that can only ever return nothing is never offered, and the
   empty state says which of the two empties it is (UI-STANDARD §5). */
.cab-sift { display: flex; gap: 7px; overflow-x: auto; scrollbar-width: none; padding: 14px 0 2px; }
.cab-sift::-webkit-scrollbar { display: none; }
.cab-sift > * { min-width: 0; }
.cab-sift button {
    flex: 0 0 auto;
    padding: 6px 13px;
    min-height: 34px;
    border: 1px solid var(--cr-line);
    border-radius: 999px;
    background: transparent;
    color: var(--cr-ink-3);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 700;
    cursor: pointer;
    font-variant-numeric: tabular-nums;
}
.cab-sift button[aria-pressed="true"] { background: var(--cr-accent-soft); border-color: var(--cr-accent-line); color: var(--cr-accent); }

.cab-empty { text-align: center; padding: 30px 16px; margin-top: 10px; border: 1px dashed var(--cr-line); border-radius: var(--cr-r-lg); }
.cab-empty b { display: block; font-family: Fraunces, Georgia, serif; font-size: 1rem; font-weight: 600; }
.cab-empty p { margin: 6px auto 0; font-size: 0.8125rem; line-height: 1.55; color: var(--cr-ink-3); max-width: 34ch; }

/* ── Everyone's cabinet ──────────────────────────────────────────────────
   NOT a leaderboard. The tab this replaces ranked an eight-year-old
   against two adults on lifetime XP — a race he cannot win and was never
   entered in. Each person gets their own shelf, in household order, and
   the only number on the row is how many they hold. */
.cab-folk {
    display: flex;
    align-items: center;
    gap: 13px;
    width: 100%;
    padding: 14px;
    border: 1px solid var(--cr-line-soft);
    border-radius: var(--cr-r);
    background: var(--cr-surface);
    color: inherit;
    font: inherit;
    text-align: left;
    text-decoration: none;
    cursor: default;
}
.cab-folk > * { min-width: 0; }
.cab-folk--you { border-color: var(--cr-accent-line); }
.cab-folk__b { flex: 1 1 auto; min-width: 0; }
.cab-folk__b p { margin: 0; font-size: 0.9375rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cab-folk__b p span { margin-left: 7px; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--cr-ink-3); }
.cab-folk__b small { display: block; margin-top: 2px; font-size: 0.75rem; color: var(--cr-ink-3); font-variant-numeric: tabular-nums; }
.cab-face {
    width: 42px;
    height: 42px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: 50%;
    overflow: hidden;
    background: var(--cr-surface-2);
    color: var(--cr-ink-2);
    font-size: 1.125rem;
    font-weight: 700;
}
.cab-face img { width: 100%; height: 100%; object-fit: cover; }
.cab-strip { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; }
.cab-strip > * { min-width: 0; }
.cab-strip i {
    width: 26px;
    height: 26px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    font-style: normal;
    font-size: 0.8125rem;
    background: linear-gradient(158deg, var(--cr-gold-2), #a9761f);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
}
.cab-strip i.is-off { background: var(--cr-surface-2); box-shadow: inset 0 2px 4px rgba(36, 28, 51, 0.17); }
.cab-strip i.is-off span { opacity: 0.28; filter: grayscale(1); }
.cab-stack { display: flex; flex-direction: column; gap: 9px; }
.cab-stack > * { min-width: 0; }

/* ── One trophy, its own page ────────────────────────────────────────────
   /badges/:slug. Reached by tapping the object — and far more often from
   the notification that said you had earned it, which is why the thing
   that awarded it is a quiet second link and never the headline. */
.cab-detail { display: grid; grid-template-columns: 1fr; gap: 14px; align-items: start; }
.cab-detail > * { min-width: 0; }
.cab-plinth {
    position: relative;
    overflow: hidden;
    padding: 30px 22px 26px;
    border-radius: var(--cr-r-xl);
    text-align: center;
    color: var(--cr-on-jewel);
    background: radial-gradient(120% 100% at 50% -10%, #3a2c66 0%, var(--cr-plum) 38%, var(--cr-deep) 100%);
}
.cab-plinth::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: radial-gradient(52% 40% at 50% 6%, rgba(240, 208, 138, 0.2), transparent 68%);
}
.cab-plinth > * { position: relative; z-index: 1; min-width: 0; }
.cab-plinth .cab-med { margin: 0 auto; }
.cab-plinth h1 { margin: 26px 0 0; font-family: Fraunces, Georgia, serif; font-size: 1.625rem; font-weight: 800; letter-spacing: -0.02em; line-height: 1.12; }
.cab-plinth .cab-plinth__sub { margin: 9px auto 0; font-size: 0.875rem; line-height: 1.6; color: var(--cr-on-jewel-mute); max-width: 34ch; }
.cab-state {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin-top: 16px;
    padding: 7px 14px;
    border-radius: 999px;
    font-size: 0.8125rem;
    font-weight: 700;
    background: rgba(240, 208, 138, 0.14);
    border: 1px solid rgba(240, 208, 138, 0.3);
    color: var(--cr-gold-2);
}
.cab-state--no { background: rgba(244, 239, 228, 0.07); border-color: var(--cr-jewel-line); color: var(--cr-on-jewel-mute); }

.cab-fact { padding: 16px 18px; border: 1px solid var(--cr-line-soft); border-radius: var(--cr-r-lg); background: var(--cr-surface); }
.cab-fact + .cab-fact { margin-top: 10px; }
.cab-fact h2 { margin: 0; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; color: var(--cr-ink-3); }
.cab-fact p { margin: 8px 0 0; font-size: 0.9375rem; line-height: 1.6; color: var(--cr-ink); }
.cab-fact p b { font-family: Fraunces, Georgia, serif; font-weight: 600; }
.cab-meter { display: block; height: 7px; border-radius: 999px; background: var(--cr-surface-2); overflow: hidden; margin-top: 12px; box-shadow: inset 0 1px 3px rgba(36, 28, 51, 0.17); }
.cab-meter i { display: block; height: 100%; background: linear-gradient(90deg, #a9761f, var(--cr-gold)); }
.cab-fact__mn { margin-top: 8px; font-size: 0.75rem; color: var(--cr-ink-3); font-variant-numeric: tabular-nums; }
.cab-holders { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
.cab-holders > * { min-width: 0; }
.cab-holder { display: flex; align-items: center; gap: 8px; padding: 7px 12px 7px 7px; border-radius: 999px; background: var(--cr-surface-2); }
.cab-holder .cab-face { width: 28px; height: 28px; font-size: 0.8125rem; background: var(--cr-bg); }
.cab-holder b { font-size: 0.8125rem; font-weight: 700; }
.cab-holder small { font-size: 0.75rem; color: var(--cr-ink-3); }
.cab-holder--no { opacity: 0.6; }

/* ── Desktop ─────────────────────────────────────────────────────────────
   The guards above are all outside this query on purpose: a min-width
   block protects the width that was never in trouble. */
@media (min-width: 720px) {
    .cab-top { position: static; background: none; border: 0; padding: 8px 0 6px; }
    .cab-top .cab-back { display: none; }
    .cab-top h1 { font-size: 1.625rem; font-weight: 800; letter-spacing: -0.02em; }
    .cab-case { display: grid; grid-template-columns: 1.1fr 1fr; gap: 32px; align-items: center; padding: 26px 30px 24px; }
    .cab-case__rank { font-size: 2rem; }
    .cab-next { margin: 0; padding: 16px; border: 1px solid var(--cr-jewel-line); border-radius: var(--cr-r-lg); background: rgba(244, 239, 228, 0.04); }
    .cab-shelf { grid-template-columns: repeat(4, 1fr); }
    .cab-switch { max-width: 520px; }
    .cab-detail { grid-template-columns: 320px 1fr; gap: 30px; }
}

@media (prefers-reduced-motion: reduce) {
    .cab-tro { transition: none; }
    .cab-tro:hover { transform: none; }
}

/* The case's own skeleton. The rank is the one line that cannot be drawn
   before the data lands — the sockets below it are already the truth. */
.cab-skel-rank { display: block; height: 30px; width: min(220px, 70%); margin-top: 10px; border-radius: 8px; }

/* A trophy with no page behind it is not drawn as something you can press.
   A tile that opens nothing is worse than one that never looked pressable
   (UI-STANDARD §3), and the same goes for the case's row when the closest
   thing to winning happens to be one of the derived trophies. */
.cab-tro--static { cursor: default; }
.cab-tro--static:hover { transform: none; }
.cab-tro--static:hover .cab-tro__n { color: inherit; }
div.cab-next { cursor: default; }

/* Empty because a filter excluded everything — so the way out of it is the
   filter, not a shrug (UI-STANDARD §5). */
.cab-clear {
    margin-top: 12px;
    padding: 8px 16px;
    min-height: 34px;
    border: 1px solid var(--cr-accent-line);
    border-radius: 999px;
    background: var(--cr-accent-soft);
    color: var(--cr-accent);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 700;
    cursor: pointer;
}
.cab-clear:focus-visible { outline: 2px solid var(--cr-gold); outline-offset: 2px; }

/* The two ways off a trophy's page: the quest that awarded it, then the
   wall. Stacked, because on a phone they are the width of the screen. */
.cab-detail .cr-btn { margin-top: 10px; }

/* ════════════════════════════════════════════════════════════════════════
   THE SERIF, APP-WIDE
   ════════════════════════════════════════════════════════════════════════

   Colour alone does not carry a voice. The Crest's is Fraunces over Plus
   Jakarta Sans — a serif for the things a page IS, a sans for everything a
   page says — and that pairing is most of what makes the profile feel unlike
   the app around it.

   Titles only: .t-display, .t-h1 and .t-h2 are the page and its sections.
   .t-h3 and below stay sans, because a card heading sits inside a paragraph's
   rhythm and a serif there reads as emphasis nobody asked for.

   Scoped to body[data-ui="v2"], so a legacy page that has not been through
   the rebuild keeps the type it was designed with rather than getting half a
   redesign. Fraunces is already in every v2 page's font link, so this costs
   no new request.

   Junior is the exception, as it is for the body font: Nunito all the way
   down. A rounder voice is the point there, and a serif is the opposite of
   one. */
/* Two families of heading class exist and both are in use: .ui-display /
   .ui-h1 / .ui-h2 from this stylesheet, and .t-display / .t-h1 / .t-h2 from
   typography.css. A rule that named only one would have re-voiced half the
   app and left the other half in the sans — which is the same page in two
   typefaces, the exact failure this change exists to end. */
body[data-ui="v2"]:not([data-portal="junior"]) .ui-display,
body[data-ui="v2"]:not([data-portal="junior"]) .ui-h1,
body[data-ui="v2"]:not([data-portal="junior"]) .ui-h2,
body[data-ui="v2"]:not([data-portal="junior"]) .t-display,
body[data-ui="v2"]:not([data-portal="junior"]) .t-h1,
body[data-ui="v2"]:not([data-portal="junior"]) .t-h2 {
    font-family: Fraunces, 'Plus Jakarta Sans', Georgia, serif;
    font-optical-sizing: auto;
}

/* .ui-h3 and below stay sans on purpose: a card heading sits inside a
   paragraph's rhythm, and a serif there reads as emphasis nobody asked for. */

/* Fraunces at 800 is heavier on the page than Plus Jakarta at 800, and the
   display size is where that reads as shouting rather than as weight. */
body[data-ui="v2"]:not([data-portal="junior"]) .ui-display,
body[data-ui="v2"]:not([data-portal="junior"]) .t-display { letter-spacing: -0.025em; }

/* ════════════════════════════════════════════════════════════════════════
   THE SLATE RAMP, WARMED
   ════════════════════════════════════════════════════════════════════════

   Two pages — modules/quest-single.html (≈500 uses) and modules/quests.html
   (≈33) — are painted in Tailwind's slate utilities rather than in --ui-*
   tokens. On the ground they were built for, white cards on a near-white
   page, that was invisible. On warm paper a white card is a bright rectangle
   with a cold edge, and slate-400 body text reads blue against plum ink.

   Rewriting 533 class names would be five hundred chances to break a layout
   for a colour change. Tailwind v4 compiles every colour utility through a
   custom property — `.bg-white{background-color:var(--color-white)}` — so
   the ramp itself is re-tinted here instead, and every one of those uses
   follows. One block, no markup touched, no rebuild needed: ui.css loads
   after tailwind.css, and custom properties inherit.

   The ramp keeps its lightness order exactly, so anything that relied on
   slate-100 being lighter than slate-700 still does. Only the hue moves:
   parchment at the light end, the crest's plum at the dark end.

   Scoped to v2 pages. A legacy page keeps true slate, because it was
   designed against true slate and half a re-tint is worse than none.

   This is a bridge, not a destination: the two quest pages should move onto
   --ui-* tokens like every other page, and this block can go the day they do. */
body[data-ui="v2"] {
    --color-white: #fffdf8;

    --color-slate-50:  #faf6ee;
    --color-slate-100: #f1e9da;
    --color-slate-200: #e4dac7;
    --color-slate-300: #c9bfae;
    --color-slate-400: #8d84a1;
    --color-slate-500: #7d7391;
    --color-slate-600: #6f6584;
    --color-slate-700: #554b6b;
    --color-slate-800: #2a2049;
    --color-slate-900: #201839;
    --color-slate-950: #120e24;

    --color-gray-50:  #faf6ee;
    --color-gray-100: #f1e9da;
    --color-gray-200: #e4dac7;
    --color-gray-300: #c9bfae;
    --color-gray-400: #8d84a1;
    --color-gray-500: #7d7391;
    --color-gray-600: #6f6584;
    --color-gray-700: #554b6b;
    --color-gray-800: #2a2049;
    --color-gray-900: #201839;
    --color-gray-950: #120e24;
}
